Merge branch 'dev' of https://github.com/hawkeye217/frigate into lost-object-zoom

This commit is contained in:
Josh Hawkins 2023-10-22 11:08:10 -05:00
commit 1d59a30a44
40 changed files with 1095 additions and 570 deletions

View File

@ -65,7 +65,7 @@ jobs:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0 uses: actions/setup-python@v4.7.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install requirements - name: Install requirements

View File

@ -33,7 +33,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \
FROM scratch AS go2rtc FROM scratch AS go2rtc
ARG TARGETARCH ARG TARGETARCH
WORKDIR /rootfs/usr/local/go2rtc/bin WORKDIR /rootfs/usr/local/go2rtc/bin
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.7.1/go2rtc_linux_${TARGETARCH}" go2rtc ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.8.1/go2rtc_linux_${TARGETARCH}" go2rtc
#### ####

View File

@ -55,24 +55,16 @@ fi
# arch specific packages # arch specific packages
if [[ "${TARGETARCH}" == "amd64" ]]; then if [[ "${TARGETARCH}" == "amd64" ]]; then
# use debian bookworm for AMD hwaccel packages # use debian bookworm for hwaccel packages
echo 'deb https://deb.debian.org/debian bookworm main contrib' >/etc/apt/sources.list.d/debian-bookworm.list echo 'deb https://deb.debian.org/debian bookworm main contrib non-free' >/etc/apt/sources.list.d/debian-bookworm.list
apt-get -qq update apt-get -qq update
apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers radeontop
rm -f /etc/apt/sources.list.d/debian-bookworm.list
# Use debian testing repo only for intel hwaccel packages
echo 'deb http://deb.debian.org/debian testing main non-free' >/etc/apt/sources.list.d/debian-testing.list
apt-get -qq update
# intel-opencl-icd specifically for GPU support in OpenVino
apt-get -qq install --no-install-recommends --no-install-suggests -y \ apt-get -qq install --no-install-recommends --no-install-suggests -y \
intel-opencl-icd \ intel-opencl-icd \
libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1 intel-gpu-tools mesa-va-drivers radeontop libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1 intel-gpu-tools
# something about this dependency requires it to be installed in a separate call rather than in the line above # something about this dependency requires it to be installed in a separate call rather than in the line above
apt-get -qq install --no-install-recommends --no-install-suggests -y \ apt-get -qq install --no-install-recommends --no-install-suggests -y \
i965-va-driver-shaders i965-va-driver-shaders
rm -f /etc/apt/sources.list.d/debian-testing.list rm -f /etc/apt/sources.list.d/debian-bookworm.list
fi fi
if [[ "${TARGETARCH}" == "arm64" ]]; then if [[ "${TARGETARCH}" == "arm64" ]]; then

View File

@ -1,3 +1,3 @@
black == 23.3.* black == 23.10.*
isort isort
ruff ruff

View File

@ -2,12 +2,12 @@ click == 8.1.*
Flask == 2.3.* Flask == 2.3.*
imutils == 0.5.* imutils == 0.5.*
matplotlib == 3.7.* matplotlib == 3.7.*
mypy == 1.4.1 mypy == 1.6.1
numpy == 1.23.* numpy == 1.23.*
onvif_zeep == 0.2.12 onvif_zeep == 0.2.12
opencv-python-headless == 4.7.0.* opencv-python-headless == 4.7.0.*
paho-mqtt == 1.6.* paho-mqtt == 1.6.*
peewee == 3.16.* peewee == 3.17.*
peewee_migrate == 1.12.* peewee_migrate == 1.12.*
psutil == 5.9.* psutil == 5.9.*
pydantic == 1.10.* pydantic == 1.10.*
@ -15,7 +15,7 @@ git+https://github.com/fbcotter/py3nvml#egg=py3nvml
PyYAML == 6.0.* PyYAML == 6.0.*
pytz == 2023.3 pytz == 2023.3
ruamel.yaml == 0.17.* ruamel.yaml == 0.17.*
tzlocal == 5.0.* tzlocal == 5.1
types-PyYAML == 6.0.* types-PyYAML == 6.0.*
requests == 2.31.* requests == 2.31.*
types-requests == 2.31.* types-requests == 2.31.*

View File

@ -149,62 +149,55 @@ http {
location /ws { location /ws {
proxy_pass http://mqtt_ws/; proxy_pass http://mqtt_ws/;
proxy_http_version 1.1; include proxy.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
} }
location /live/jsmpeg/ { location /live/jsmpeg/ {
proxy_pass http://jsmpeg/; proxy_pass http://jsmpeg/;
proxy_http_version 1.1; include proxy.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
} }
location /live/mse/ { location /live/mse/ {
proxy_pass http://go2rtc/; proxy_pass http://go2rtc/;
proxy_http_version 1.1; include proxy.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
} }
location /live/webrtc/ { location /live/webrtc/ {
proxy_pass http://go2rtc/; proxy_pass http://go2rtc/;
proxy_http_version 1.1; include proxy.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
} }
location ~* /api/go2rtc([/]?.*)$ { location ~* /api/go2rtc([/]?.*)$ {
proxy_pass http://go2rtc; proxy_pass http://go2rtc;
rewrite ^/api/go2rtc(.*)$ /api$1 break; rewrite ^/api/go2rtc(.*)$ /api$1 break;
proxy_http_version 1.1; include proxy.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
} }
location ~* /api/.*\.(jpg|jpeg|png)$ { location ~* /api/.*\.(jpg|jpeg|png)$ {
rewrite ^/api/(.*)$ $1 break; rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api; proxy_pass http://frigate_api;
proxy_pass_request_headers on; include proxy.conf;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
location /api/ { location /api/ {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off; expires off;
proxy_pass http://frigate_api/; proxy_pass http://frigate_api/;
proxy_pass_request_headers on; include proxy.conf;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location /api/stats {
proxy_set_header X-Forwarded-Proto $scheme; access_log off;
rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api;
include proxy.conf;
}
location /api/version {
access_log off;
rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api;
include proxy.conf;
}
} }
location / { location / {

View File

@ -0,0 +1,4 @@
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;

View File

@ -120,7 +120,7 @@ NOTE: The folder that is mapped from the host needs to be the folder that contai
## Custom go2rtc version ## Custom go2rtc version
Frigate currently includes go2rtc v1.7.1, there may be certain cases where you want to run a different version of go2rtc. Frigate currently includes go2rtc v1.8.1, there may be certain cases where you want to run a different version of go2rtc.
To do this: To do this:

View File

@ -140,7 +140,7 @@ go2rtc:
- rtspx://192.168.1.1:7441/abcdefghijk - rtspx://192.168.1.1:7441/abcdefghijk
``` ```
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-rtsp) [See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#source-rtsp)
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record and rtmp if used directly with unifi protect. In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record and rtmp if used directly with unifi protect.

View File

@ -436,7 +436,7 @@ rtmp:
enabled: False enabled: False
# Optional: Restream configuration # Optional: Restream configuration
# Uses https://github.com/AlexxIT/go2rtc (v1.7.1) # Uses https://github.com/AlexxIT/go2rtc (v1.8.1)
go2rtc: go2rtc:
# Optional: jsmpeg stream configuration for WebUI # Optional: jsmpeg stream configuration for WebUI

View File

@ -115,4 +115,4 @@ services:
::: :::
See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#module-webrtc) for more information about this. See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#module-webrtc) for more information about this.

View File

@ -7,7 +7,7 @@ title: Restream
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.7.1) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#configuration) for more advanced configurations and features. Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.8.1) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#configuration) for more advanced configurations and features.
:::note :::note
@ -138,7 +138,7 @@ cameras:
## Advanced Restream Configurations ## Advanced Restream Configurations
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
NOTE: The output will need to be passed with two curly braces `{{output}}` NOTE: The output will need to be passed with two curly braces `{{output}}`

View File

@ -11,7 +11,7 @@ Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect
# Setup a go2rtc stream # Setup a go2rtc stream
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. If you set the stream name under go2rtc to match the name of your camera, it will automatically be mapped and you will get additional live view options for the camera. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#module-streams), not just rtsp. First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. If you set the stream name under go2rtc to match the name of your camera, it will automatically be mapped and you will get additional live view options for the camera. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#module-streams), not just rtsp.
```yaml ```yaml
go2rtc: go2rtc:
@ -24,7 +24,7 @@ The easiest live view to get working is MSE. After adding this to the config, re
### What if my video doesn't play? ### What if my video doesn't play?
If you are unable to see your video feed, first check the go2rtc logs in the Frigate UI under Logs in the sidebar. If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. If you do not see any errors, then the video codec of the stream may not be supported in your browser. If your camera stream is set to H265, try switching to H264. You can see more information about [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#codecs-madness) in the go2rtc documentation. If you are not able to switch your camera settings from H265 to H264 or your stream is a different format such as MJPEG, you can use go2rtc to re-encode the video using the [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. Here is an example of a config that will re-encode the stream to H264 without hardware acceleration: If you are unable to see your video feed, first check the go2rtc logs in the Frigate UI under Logs in the sidebar. If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. If you do not see any errors, then the video codec of the stream may not be supported in your browser. If your camera stream is set to H265, try switching to H264. You can see more information about [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#codecs-madness) in the go2rtc documentation. If you are not able to switch your camera settings from H265 to H264 or your stream is a different format such as MJPEG, you can use go2rtc to re-encode the video using the [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. Here is an example of a config that will re-encode the stream to H264 without hardware acceleration:
```yaml ```yaml
go2rtc: go2rtc:

View File

@ -23,6 +23,17 @@ Ensure your cameras send h264 encoded video, or [transcode them](/configuration/
You can open `chrome://media-internals/` in another tab and then try to playback, the media internals page will give information about why playback is failing. You can open `chrome://media-internals/` in another tab and then try to playback, the media internals page will give information about why playback is failing.
### What do I do if my cameras sub stream is not good enough?
Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that a suitable resolution, the main stream can be resized.
To do this efficiently the following setup is required:
1. A GPU or iGPU must be available to do the scaling.
2. [ffmpeg presets for hwaccel](/configuration/hardware_acceleration.md) must be used
3. Set the desired detection resolution for `detect -> width` and `detect -> height`.
When this is done correctly, the GPU will do the decoding and scaling which will result in a small increase in CPU usage but with better results.
### My mjpeg stream or snapshots look green and crazy ### My mjpeg stream or snapshots look green and crazy
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards. This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards.

View File

@ -21,7 +21,7 @@ module.exports = {
{ {
type: "link", type: "link",
label: "Go2RTC Configuration Reference", label: "Go2RTC Configuration Reference",
href: "https://github.com/AlexxIT/go2rtc/tree/v1.7.1#configuration", href: "https://github.com/AlexxIT/go2rtc/tree/v1.8.1#configuration",
}, },
], ],
Detectors: [ Detectors: [

View File

@ -36,7 +36,7 @@ from frigate.events.external import ExternalEventProcessor
from frigate.events.maintainer import EventProcessor from frigate.events.maintainer import EventProcessor
from frigate.http import create_app from frigate.http import create_app
from frigate.log import log_process, root_configurer from frigate.log import log_process, root_configurer
from frigate.models import Event, Recordings, RecordingsToDelete, Timeline from frigate.models import Event, Recordings, RecordingsToDelete, Regions, Timeline
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output import output_frames from frigate.output import output_frames
@ -49,6 +49,7 @@ from frigate.stats import StatsEmitter, stats_init
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
from frigate.util.object import get_camera_regions_grid
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog from frigate.watchdog import FrigateWatchdog
@ -69,6 +70,7 @@ class FrigateApp:
self.feature_metrics: dict[str, FeatureMetricsTypes] = {} self.feature_metrics: dict[str, FeatureMetricsTypes] = {}
self.ptz_metrics: dict[str, PTZMetricsTypes] = {} self.ptz_metrics: dict[str, PTZMetricsTypes] = {}
self.processes: dict[str, int] = {} self.processes: dict[str, int] = {}
self.region_grids: dict[str, list[list[dict[str, int]]]] = {}
def set_environment_vars(self) -> None: def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items(): for key, value in self.config.environment_vars.items():
@ -161,6 +163,7 @@ class FrigateApp:
# issue https://github.com/python/typeshed/issues/8799 # issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards # from mypy 0.981 onwards
"frame_queue": mp.Queue(maxsize=2), "frame_queue": mp.Queue(maxsize=2),
"region_grid_queue": mp.Queue(maxsize=1),
"capture_process": None, "capture_process": None,
"process": None, "process": None,
"audio_rms": mp.Value("d", 0.0), # type: ignore[typeddict-item] "audio_rms": mp.Value("d", 0.0), # type: ignore[typeddict-item]
@ -333,7 +336,7 @@ class FrigateApp:
60, 10 * len([c for c in self.config.cameras.values() if c.enabled]) 60, 10 * len([c for c in self.config.cameras.values() if c.enabled])
), ),
) )
models = [Event, Recordings, RecordingsToDelete, Timeline] models = [Event, Recordings, RecordingsToDelete, Regions, Timeline]
self.db.bind(models) self.db.bind(models)
def init_stats(self) -> None: def init_stats(self) -> None:
@ -458,6 +461,17 @@ class FrigateApp:
output_processor.start() output_processor.start()
logger.info(f"Output process started: {output_processor.pid}") logger.info(f"Output process started: {output_processor.pid}")
def init_historical_regions(self) -> None:
# delete region grids for removed or renamed cameras
cameras = list(self.config.cameras.keys())
Regions.delete().where(~(Regions.camera << cameras)).execute()
# create or update region grids for each camera
for camera in self.config.cameras.values():
self.region_grids[camera.name] = get_camera_regions_grid(
camera.name, camera.detect
)
def start_camera_processors(self) -> None: def start_camera_processors(self) -> None:
for name, config in self.config.cameras.items(): for name, config in self.config.cameras.items():
if not self.config.cameras[name].enabled: if not self.config.cameras[name].enabled:
@ -475,8 +489,10 @@ class FrigateApp:
self.detection_queue, self.detection_queue,
self.detection_out_events[name], self.detection_out_events[name],
self.detected_frames_queue, self.detected_frames_queue,
self.inter_process_queue,
self.camera_metrics[name], self.camera_metrics[name],
self.ptz_metrics[name], self.ptz_metrics[name],
self.region_grids[name],
), ),
) )
camera_process.daemon = True camera_process.daemon = True
@ -617,6 +633,7 @@ class FrigateApp:
self.start_detectors() self.start_detectors()
self.start_video_output_processor() self.start_video_output_processor()
self.start_ptz_autotracker() self.start_ptz_autotracker()
self.init_historical_regions()
self.start_detected_frames_processor() self.start_detected_frames_processor()
self.start_camera_processors() self.start_camera_processors()
self.start_camera_capture_processes() self.start_camera_capture_processes()

View File

@ -5,10 +5,11 @@ from abc import ABC, abstractmethod
from typing import Any, Callable from typing import Any, Callable
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import INSERT_MANY_RECORDINGS from frigate.const import INSERT_MANY_RECORDINGS, REQUEST_REGION_GRID
from frigate.models import Recordings from frigate.models import Recordings
from frigate.ptz.onvif import OnvifCommandEnum, OnvifController from frigate.ptz.onvif import OnvifCommandEnum, OnvifController
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, FeatureMetricsTypes, PTZMetricsTypes
from frigate.util.object import get_camera_regions_grid
from frigate.util.services import restart_frigate from frigate.util.services import restart_frigate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -90,6 +91,11 @@ class Dispatcher:
restart_frigate() restart_frigate()
elif topic == INSERT_MANY_RECORDINGS: elif topic == INSERT_MANY_RECORDINGS:
Recordings.insert_many(payload).execute() Recordings.insert_many(payload).execute()
elif topic == REQUEST_REGION_GRID:
camera = payload
self.camera_metrics[camera]["region_grid_queue"].put(
get_camera_regions_grid(camera, self.config.cameras[camera].detect)
)
else: else:
self.publish(topic, payload, retain=False) self.publish(topic, payload, retain=False)

View File

@ -85,7 +85,10 @@ class WebSocketClient(Communicator): # type: ignore[misc]
logger.debug(f"payload for {topic} wasn't text. Skipping...") logger.debug(f"payload for {topic} wasn't text. Skipping...")
return return
try:
self.websocket_server.manager.broadcast(ws_message) self.websocket_server.manager.broadcast(ws_message)
except ConnectionResetError:
pass
def stop(self) -> None: def stop(self) -> None:
self.websocket_server.manager.close_all() self.websocket_server.manager.close_all()

View File

@ -12,7 +12,7 @@ FRIGATE_LOCALHOST = "http://127.0.0.1:5000"
PLUS_ENV_VAR = "PLUS_API_KEY" PLUS_ENV_VAR = "PLUS_API_KEY"
PLUS_API_HOST = "https://api.frigate.video" PLUS_API_HOST = "https://api.frigate.video"
# Attributes # Attribute & Object Consts
ATTRIBUTE_LABEL_MAP = { ATTRIBUTE_LABEL_MAP = {
"person": ["face", "amazon"], "person": ["face", "amazon"],
@ -21,6 +21,11 @@ ATTRIBUTE_LABEL_MAP = {
ALL_ATTRIBUTE_LABELS = [ ALL_ATTRIBUTE_LABELS = [
item for sublist in ATTRIBUTE_LABEL_MAP.values() for item in sublist item for sublist in ATTRIBUTE_LABEL_MAP.values() for item in sublist
] ]
LABEL_CONSOLIDATION_MAP = {
"car": 0.8,
"face": 0.5,
}
LABEL_CONSOLIDATION_DEFAULT = 0.9
# Audio Consts # Audio Consts
@ -51,6 +56,7 @@ MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to
# Internal Comms Topics # Internal Comms Topics
INSERT_MANY_RECORDINGS = "insert_many_recordings" INSERT_MANY_RECORDINGS = "insert_many_recordings"
REQUEST_REGION_GRID = "request_region_grid"
# Autotracking # Autotracking

View File

@ -83,15 +83,20 @@ class EventCleanup(threading.Thread):
datetime.datetime.now() - datetime.timedelta(days=expire_days) datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() ).timestamp()
# grab all events after specific time # grab all events after specific time
expired_events = Event.select( expired_events = (
Event.select(
Event.id, Event.id,
Event.camera, Event.camera,
).where( )
.where(
Event.camera.not_in(self.camera_keys), Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == event.label, Event.label == event.label,
Event.retain_indefinitely == False, Event.retain_indefinitely == False,
) )
.namedtuples()
.iterator()
)
# delete the media from disk # delete the media from disk
for event in expired_events: for event in expired_events:
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
@ -136,15 +141,20 @@ class EventCleanup(threading.Thread):
datetime.datetime.now() - datetime.timedelta(days=expire_days) datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() ).timestamp()
# grab all events after specific time # grab all events after specific time
expired_events = Event.select( expired_events = (
Event.select(
Event.id, Event.id,
Event.camera, Event.camera,
).where( )
.where(
Event.camera == name, Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == event.label, Event.label == event.label,
Event.retain_indefinitely == False, Event.retain_indefinitely == False,
) )
.namedtuples()
.iterator()
)
# delete the grabbed clips from disk # delete the grabbed clips from disk
# only snapshots are stored in /clips # only snapshots are stored in /clips

View File

@ -261,7 +261,7 @@ def send_to_plus(id):
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return make_response(
jsonify({"success": False, "message": str(ex)}), jsonify({"success": False, "message": "Error uploading image"}),
400, 400,
) )
@ -281,7 +281,7 @@ def send_to_plus(id):
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return make_response(
jsonify({"success": False, "message": str(ex)}), jsonify({"success": False, "message": "Error uploading annotation"}),
400, 400,
) )
@ -352,7 +352,7 @@ def false_positive(id):
except Exception as ex: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return make_response( return make_response(
jsonify({"success": False, "message": str(ex)}), jsonify({"success": False, "message": "Error uploading false positive"}),
400, 400,
) )
@ -455,8 +455,9 @@ def get_labels():
else: else:
events = Event.select(Event.label).distinct() events = Event.select(Event.label).distinct()
except Exception as e: except Exception as e:
logger.error(e)
return make_response( return make_response(
jsonify({"success": False, "message": f"Failed to get labels: {e}"}), 404 jsonify({"success": False, "message": "Failed to get labels"}), 404
) )
labels = sorted([e.label for e in events]) labels = sorted([e.label for e in events])
@ -469,9 +470,9 @@ def get_sub_labels():
try: try:
events = Event.select(Event.sub_label).distinct() events = Event.select(Event.sub_label).distinct()
except Exception as e: except Exception:
return make_response( return make_response(
jsonify({"success": False, "message": f"Failed to get sub_labels: {e}"}), jsonify({"success": False, "message": "Failed to get sub_labels"}),
404, 404,
) )
@ -516,6 +517,7 @@ def delete_event(id):
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
event.delete_instance() event.delete_instance()
Timeline.delete().where(Timeline.source_id == id).execute()
return make_response( return make_response(
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200 jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
) )
@ -648,7 +650,7 @@ def event_snapshot(id):
) )
# read snapshot from disk # read snapshot from disk
with open( with open(
os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), "rb" os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.jpg"), "rb"
) as image_file: ) as image_file:
jpg_bytes = image_file.read() jpg_bytes = image_file.read()
except DoesNotExist: except DoesNotExist:
@ -740,7 +742,7 @@ def event_clip(id):
jsonify({"success": False, "message": "Clip not available"}), 404 jsonify({"success": False, "message": "Clip not available"}), 404
) )
file_name = f"{event.camera}-{id}.mp4" file_name = f"{event.camera}-{event.id}.mp4"
clip_path = os.path.join(CLIPS_DIR, file_name) clip_path = os.path.join(CLIPS_DIR, file_name)
if not os.path.isfile(clip_path): if not os.path.isfile(clip_path):
@ -956,9 +958,10 @@ def events():
.order_by(Event.start_time.desc()) .order_by(Event.start_time.desc())
.limit(limit) .limit(limit)
.dicts() .dicts()
.iterator()
) )
return jsonify([e for e in events]) return jsonify(list(events))
@bp.route("/events/<camera_name>/<label>/create", methods=["POST"]) @bp.route("/events/<camera_name>/<label>/create", methods=["POST"])
@ -993,8 +996,9 @@ def create_event(camera_name, label):
frame, frame,
) )
except Exception as e: except Exception as e:
logger.error(e)
return make_response( return make_response(
jsonify({"success": False, "message": f"An unknown error occurred: {e}"}), jsonify({"success": False, "message": "An unknown error occurred"}),
500, 500,
) )
@ -1187,11 +1191,12 @@ def config_set():
with open(config_file, "w") as f: with open(config_file, "w") as f:
f.write(old_raw_config) f.write(old_raw_config)
f.close() f.close()
logger.error(f"\nConfig Error:\n\n{str(traceback.format_exc())}")
return make_response( return make_response(
jsonify( jsonify(
{ {
"success": False, "success": False,
"message": f"\nConfig Error:\n\n{str(traceback.format_exc())}", "message": "Error parsing config. Check logs for error message.",
} }
), ),
400, 400,
@ -1365,7 +1370,10 @@ def latest_frame(camera_name):
@bp.route("/<camera_name>/recordings/<frame_time>/snapshot.png") @bp.route("/<camera_name>/recordings/<frame_time>/snapshot.png")
def get_snapshot_from_recording(camera_name: str, frame_time: str): def get_snapshot_from_recording(camera_name: str, frame_time: str):
if camera_name not in current_app.frigate_config.cameras: if camera_name not in current_app.frigate_config.cameras:
return "Camera named {} not found".format(camera_name), 404 return make_response(
jsonify({"success": False, "message": "Camera not found"}),
404,
)
frame_time = float(frame_time) frame_time = float(frame_time)
recording_query = ( recording_query = (
@ -1483,6 +1491,7 @@ def recordings_summary(camera_name):
), ),
).desc() ).desc()
) )
.namedtuples()
) )
event_groups = ( event_groups = (
@ -1504,14 +1513,14 @@ def recordings_summary(camera_name):
), ),
), ),
) )
.objects() .namedtuples()
) )
event_map = {g.hour: g.count for g in event_groups} event_map = {g.hour: g.count for g in event_groups}
days = {} days = {}
for recording_group in recording_groups.objects(): for recording_group in recording_groups:
parts = recording_group.hour.split() parts = recording_group.hour.split()
hour = parts[1] hour = parts[1]
day = parts[0] day = parts[0]
@ -1555,9 +1564,11 @@ def recordings(camera_name):
Recordings.start_time <= before, Recordings.start_time <= before,
) )
.order_by(Recordings.start_time) .order_by(Recordings.start_time)
.dicts()
.iterator()
) )
return jsonify([e for e in recordings.dicts()]) return jsonify(list(recordings))
@bp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4") @bp.route("/<camera_name>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
@ -1591,7 +1602,7 @@ def recording_clip(camera_name, start_ts, end_ts):
if clip.end_time > end_ts: if clip.end_time > end_ts:
playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}") playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}")
file_name = f"clip_{camera_name}_{start_ts}-{end_ts}.mp4" file_name = secure_filename(f"clip_{camera_name}_{start_ts}-{end_ts}.mp4")
path = os.path.join(CACHE_DIR, file_name) path = os.path.join(CACHE_DIR, file_name)
if not os.path.exists(path): if not os.path.exists(path):
@ -1662,6 +1673,7 @@ def vod_ts(camera_name, start_ts, end_ts):
) )
.where(Recordings.camera == camera_name) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
.iterator()
) )
clips = [] clips = []
@ -1759,16 +1771,17 @@ def vod_event(id):
404, 404,
) )
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4") clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.mp4")
if not os.path.isfile(clip_path): if not os.path.isfile(clip_path):
end_ts = ( end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time datetime.now().timestamp() if event.end_time is None else event.end_time
) )
vod_response = vod_ts(event.camera, event.start_time, end_ts) vod_response = vod_ts(event.camera, event.start_time, end_ts)
# If the recordings are not found, set has_clip to false # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false
if ( if (
type(vod_response) == tuple event.start_time < datetime.now().timestamp() - 300
and type(vod_response) == tuple
and len(vod_response) == 2 and len(vod_response) == 2
and vod_response[1] == 404 and vod_response[1] == 404
): ):
@ -1977,7 +1990,8 @@ def logs(service: str):
file.close() file.close()
return contents, 200 return contents, 200
except FileNotFoundError as e: except FileNotFoundError as e:
logger.error(e)
return make_response( return make_response(
jsonify({"success": False, "message": f"Could not find log file: {e}"}), jsonify({"success": False, "message": "Could not find log file"}),
500, 500,
) )

View File

@ -57,6 +57,12 @@ class Timeline(Model): # type: ignore[misc]
data = JSONField() # ex: tracked object id, region, box, etc. data = JSONField() # ex: tracked object id, region, box, etc.
class Regions(Model): # type: ignore[misc]
camera = CharField(null=False, primary_key=True, max_length=20)
grid = JSONField() # json blob of grid
last_update = DateTimeField()
class Recordings(Model): # type: ignore[misc] class Recordings(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30) id = CharField(null=False, primary_key=True, max_length=30)
camera = CharField(index=True, max_length=20) camera = CharField(index=True, max_length=20)

View File

@ -1,3 +1,5 @@
import logging
import cv2 import cv2
import imutils import imutils
import numpy as np import numpy as np
@ -6,6 +8,8 @@ from scipy.ndimage import gaussian_filter
from frigate.config import MotionConfig from frigate.config import MotionConfig
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
logger = logging.getLogger(__name__)
class ImprovedMotionDetector(MotionDetector): class ImprovedMotionDetector(MotionDetector):
def __init__( def __init__(
@ -138,8 +142,8 @@ class ImprovedMotionDetector(MotionDetector):
self.motion_frame_size[0] * self.motion_frame_size[1] self.motion_frame_size[0] * self.motion_frame_size[1]
) )
# once the motion drops to less than 1% for the first time, assume its calibrated # once the motion is less than 5% and the number of contours is < 4, assume its calibrated
if pct_motion < 0.01: if pct_motion < 0.05 and len(motion_boxes) <= 4:
self.calibrating = False self.calibrating = False
# if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate # if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate

View File

@ -100,6 +100,17 @@ class OnvifController:
None, None,
) )
# status request for autotracking and filling ptz-parameters
status_request = ptz.create_type("GetStatus")
status_request.ProfileToken = profile.token
self.cams[camera_name]["status_request"] = status_request
try:
status = ptz.GetStatus(status_request)
logger.debug(f"Onvif status config for {camera_name}: {status}")
except Exception as e:
logger.warning(f"Unable to get status from camera: {camera_name}: {e}")
status = None
# autoracking relative panning/tilting needs a relative zoom value set to 0 # autoracking relative panning/tilting needs a relative zoom value set to 0
# if camera supports relative movement # if camera supports relative movement
if self.config.cameras[camera_name].onvif.autotracking.zooming: if self.config.cameras[camera_name].onvif.autotracking.zooming:
@ -123,9 +134,7 @@ class OnvifController:
move_request = ptz.create_type("RelativeMove") move_request = ptz.create_type("RelativeMove")
move_request.ProfileToken = profile.token move_request.ProfileToken = profile.token
if move_request.Translation is None and fov_space_id is not None: if move_request.Translation is None and fov_space_id is not None:
move_request.Translation = ptz.GetStatus( move_request.Translation = status.Position
{"ProfileToken": profile.token}
).Position
move_request.Translation.PanTilt.space = ptz_config["Spaces"][ move_request.Translation.PanTilt.space = ptz_config["Spaces"][
"RelativePanTiltTranslationSpace" "RelativePanTiltTranslationSpace"
][fov_space_id]["URI"] ][fov_space_id]["URI"]
@ -153,7 +162,7 @@ class OnvifController:
) )
if move_request.Speed is None: if move_request.Speed is None:
move_request.Speed = ptz.GetStatus({"ProfileToken": profile.token}).Position move_request.Speed = status.Position if status else None
self.cams[camera_name]["relative_move_request"] = move_request self.cams[camera_name]["relative_move_request"] = move_request
# setup absolute moving request for autotracking zooming # setup absolute moving request for autotracking zooming
@ -161,13 +170,6 @@ class OnvifController:
move_request.ProfileToken = profile.token move_request.ProfileToken = profile.token
self.cams[camera_name]["absolute_move_request"] = move_request self.cams[camera_name]["absolute_move_request"] = move_request
# status request for autotracking
status_request = ptz.create_type("GetStatus")
status_request.ProfileToken = profile.token
self.cams[camera_name]["status_request"] = status_request
status = ptz.GetStatus(status_request)
logger.debug(f"Onvif status config for {camera_name}: {status}")
# setup existing presets # setup existing presets
try: try:
presets: list[dict] = ptz.GetPresets({"ProfileToken": profile.token}) presets: list[dict] = ptz.GetPresets({"ProfileToken": profile.token})
@ -177,7 +179,7 @@ class OnvifController:
for preset in presets: for preset in presets:
self.cams[camera_name]["presets"][ self.cams[camera_name]["presets"][
getattr(preset, "Name", f"preset {preset['token']}").lower() (getattr(preset, "Name") or f"preset {preset['token']}").lower()
] = preset["token"] ] = preset["token"]
# get list of supported features # get list of supported features
@ -513,7 +515,10 @@ class OnvifController:
onvif: ONVIFCamera = self.cams[camera_name]["onvif"] onvif: ONVIFCamera = self.cams[camera_name]["onvif"]
status_request = self.cams[camera_name]["status_request"] status_request = self.cams[camera_name]["status_request"]
try:
status = onvif.get_service("ptz").GetStatus(status_request) status = onvif.get_service("ptz").GetStatus(status_request)
except Exception:
pass # We're unsupported, that'll be reported in the next check.
# there doesn't seem to be an onvif standard with this optional parameter # there doesn't seem to be an onvif standard with this optional parameter
# some cameras can report MoveStatus with or without PanTilt or Zoom attributes # some cameras can report MoveStatus with or without PanTilt or Zoom attributes

View File

@ -48,13 +48,18 @@ class RecordingCleanup(threading.Thread):
expire_before = ( expire_before = (
datetime.datetime.now() - datetime.timedelta(days=expire_days) datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() ).timestamp()
no_camera_recordings: Recordings = Recordings.select( no_camera_recordings: Recordings = (
Recordings.select(
Recordings.id, Recordings.id,
Recordings.path, Recordings.path,
).where( )
.where(
Recordings.camera.not_in(list(self.config.cameras.keys())), Recordings.camera.not_in(list(self.config.cameras.keys())),
Recordings.end_time < expire_before, Recordings.end_time < expire_before,
) )
.namedtuples()
.iterator()
)
deleted_recordings = set() deleted_recordings = set()
for recording in no_camera_recordings: for recording in no_camera_recordings:
@ -95,6 +100,8 @@ class RecordingCleanup(threading.Thread):
Recordings.end_time < expire_date, Recordings.end_time < expire_date,
) )
.order_by(Recordings.start_time) .order_by(Recordings.start_time)
.namedtuples()
.iterator()
) )
# Get all the events to check against # Get all the events to check against
@ -111,14 +118,14 @@ class RecordingCleanup(threading.Thread):
Event.has_clip, Event.has_clip,
) )
.order_by(Event.start_time) .order_by(Event.start_time)
.objects() .namedtuples()
) )
# loop over recordings and see if they overlap with any non-expired events # loop over recordings and see if they overlap with any non-expired events
# TODO: expire segments based on segment stats according to config # TODO: expire segments based on segment stats according to config
event_start = 0 event_start = 0
deleted_recordings = set() deleted_recordings = set()
for recording in recordings.objects().iterator(): for recording in recordings:
keep = False keep = False
# Now look for a reason to keep this recording segment # Now look for a reason to keep this recording segment
for idx in range(event_start, len(events)): for idx in range(event_start, len(events)):

View File

@ -163,6 +163,8 @@ class RecordingMaintainer(threading.Thread):
Event.has_clip, Event.has_clip,
) )
.order_by(Event.start_time) .order_by(Event.start_time)
.namedtuples()
.iterator()
) )
tasks.extend( tasks.extend(
@ -406,11 +408,13 @@ class RecordingMaintainer(threading.Thread):
return None return None
def run(self) -> None: def run(self) -> None:
camera_count = sum(camera.enabled for camera in self.config.cameras.values())
# Check for new files every 5 seconds # Check for new files every 5 seconds
wait_time = 0.0 wait_time = 0.0
while not self.stop_event.wait(wait_time): while not self.stop_event.wait(wait_time):
run_start = datetime.datetime.now().timestamp() run_start = datetime.datetime.now().timestamp()
stale_frame_count = 0
stale_frame_count_threshold = 10
# empty the object recordings info queue # empty the object recordings info queue
while True: while True:
try: try:
@ -420,7 +424,10 @@ class RecordingMaintainer(threading.Thread):
current_tracked_objects, current_tracked_objects,
motion_boxes, motion_boxes,
regions, regions,
) = self.object_recordings_info_queue.get(False) ) = self.object_recordings_info_queue.get(True, timeout=0.01)
if frame_time < run_start - stale_frame_count_threshold:
stale_frame_count += 1
if self.process_info[camera]["record_enabled"].value: if self.process_info[camera]["record_enabled"].value:
self.object_recordings_info[camera].append( self.object_recordings_info[camera].append(
@ -432,17 +439,32 @@ class RecordingMaintainer(threading.Thread):
) )
) )
except queue.Empty: except queue.Empty:
q_size = self.object_recordings_info_queue.qsize()
if q_size > camera_count:
logger.debug(
f"object_recordings_info loop queue not empty ({q_size})."
)
break break
if stale_frame_count > 0:
logger.warning(
f"Found {stale_frame_count} old frames, segments from recordings may be missing."
)
# empty the audio recordings info queue if audio is enabled # empty the audio recordings info queue if audio is enabled
if self.audio_recordings_info_queue: if self.audio_recordings_info_queue:
stale_frame_count = 0
while True: while True:
try: try:
( (
camera, camera,
frame_time, frame_time,
dBFS, dBFS,
) = self.audio_recordings_info_queue.get(False) ) = self.audio_recordings_info_queue.get(True, timeout=0.01)
if frame_time < run_start - stale_frame_count_threshold:
stale_frame_count += 1
if self.process_info[camera]["record_enabled"].value: if self.process_info[camera]["record_enabled"].value:
self.audio_recordings_info[camera].append( self.audio_recordings_info[camera].append(
@ -452,8 +474,18 @@ class RecordingMaintainer(threading.Thread):
) )
) )
except queue.Empty: except queue.Empty:
q_size = self.audio_recordings_info_queue.qsize()
if q_size > camera_count:
logger.debug(
f"object_recordings_info loop audio queue not empty ({q_size})."
)
break break
if stale_frame_count > 0:
logger.error(
f"Found {stale_frame_count} old audio frames, segments from recordings may be missing"
)
try: try:
asyncio.run(self.move_files()) asyncio.run(self.move_files())
except Exception as e: except Exception as e:

View File

@ -248,6 +248,7 @@ def stats_snapshot(
total_detection_fps = 0 total_detection_fps = 0
stats["cameras"] = {}
for name, camera_stats in camera_metrics.items(): for name, camera_stats in camera_metrics.items():
total_detection_fps += camera_stats["detection_fps"].value total_detection_fps += camera_stats["detection_fps"].value
pid = camera_stats["process"].pid if camera_stats["process"] else None pid = camera_stats["process"].pid if camera_stats["process"] else None
@ -259,7 +260,7 @@ def stats_snapshot(
if camera_stats["capture_process"] if camera_stats["capture_process"]
else None else None
) )
stats[name] = { stats["cameras"][name] = {
"camera_fps": round(camera_stats["camera_fps"].value, 2), "camera_fps": round(camera_stats["camera_fps"].value, 2),
"process_fps": round(camera_stats["process_fps"].value, 2), "process_fps": round(camera_stats["process_fps"].value, 2),
"skipped_fps": round(camera_stats["skipped_fps"].value, 2), "skipped_fps": round(camera_stats["skipped_fps"].value, 2),

View File

@ -99,13 +99,19 @@ class StorageMaintainer(threading.Thread):
[b["bandwidth"] for b in self.camera_storage_stats.values()] [b["bandwidth"] for b in self.camera_storage_stats.values()]
) )
recordings: Recordings = Recordings.select( recordings: Recordings = (
Recordings.select(
Recordings.id, Recordings.id,
Recordings.start_time, Recordings.start_time,
Recordings.end_time, Recordings.end_time,
Recordings.segment_size, Recordings.segment_size,
Recordings.path, Recordings.path,
).order_by(Recordings.start_time.asc()) )
.order_by(Recordings.start_time.asc())
.namedtuples()
.iterator()
)
retained_events: Event = ( retained_events: Event = (
Event.select( Event.select(
Event.start_time, Event.start_time,
@ -116,12 +122,12 @@ class StorageMaintainer(threading.Thread):
Event.has_clip, Event.has_clip,
) )
.order_by(Event.start_time.asc()) .order_by(Event.start_time.asc())
.objects() .namedtuples()
) )
event_start = 0 event_start = 0
deleted_recordings = set() deleted_recordings = set()
for recording in recordings.objects().iterator(): for recording in recordings:
# check if 1 hour of storage has been reclaimed # check if 1 hour of storage has been reclaimed
if deleted_segments_size > hourly_bandwidth: if deleted_segments_size > hourly_bandwidth:
break break
@ -162,13 +168,18 @@ class StorageMaintainer(threading.Thread):
logger.error( logger.error(
f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size} MB have been cleared. Retained recordings must be deleted." f"Could not clear {hourly_bandwidth} MB, currently {deleted_segments_size} MB have been cleared. Retained recordings must be deleted."
) )
recordings = Recordings.select( recordings = (
Recordings.select(
Recordings.id, Recordings.id,
Recordings.path, Recordings.path,
Recordings.segment_size, Recordings.segment_size,
).order_by(Recordings.start_time.asc()) )
.order_by(Recordings.start_time.asc())
.namedtuples()
.iterator()
)
for recording in recordings.objects().iterator(): for recording in recordings:
if deleted_segments_size > hourly_bandwidth: if deleted_segments_size > hourly_bandwidth:
break break

View File

@ -1,6 +1,6 @@
from unittest import TestCase, main from unittest import TestCase, main
from frigate.video import box_overlaps, reduce_boxes from frigate.util.object import box_overlaps, reduce_boxes
class TestBoxOverlaps(TestCase): class TestBoxOverlaps(TestCase):

View File

@ -6,10 +6,11 @@ from norfair.drawing.color import Palette
from norfair.drawing.drawer import Drawer from norfair.drawing.drawer import Drawer
from frigate.util.image import intersection from frigate.util.image import intersection
from frigate.video import ( from frigate.util.object import (
get_cluster_boundary, get_cluster_boundary,
get_cluster_candidates, get_cluster_candidates,
get_cluster_region, get_cluster_region,
get_region_from_grid,
) )
@ -190,3 +191,36 @@ class TestObjectBoundingBoxes(unittest.TestCase):
assert intersection(box_a, box_b) == None assert intersection(box_a, box_b) == None
assert intersection(box_b, box_c) == (899, 128, 985, 151) assert intersection(box_b, box_c) == (899, 128, 985, 151)
class TestRegionGrid(unittest.TestCase):
def setUp(self) -> None:
pass
def test_region_in_range(self):
"""Test that region is kept at minimal size when within std dev."""
frame_shape = (720, 1280)
box = [450, 450, 550, 550]
region_grid = [
[],
[],
[],
[{}, {}, {}, {}, {}, {"sizes": [0.25], "mean": 0.26, "std_dev": 0.01}],
]
region = get_region_from_grid(frame_shape, box, 320, region_grid)
assert region[2] - region[0] == 320
def test_region_out_of_range(self):
"""Test that region is upsized when outside of std dev."""
frame_shape = (720, 1280)
box = [450, 450, 550, 550]
region_grid = [
[],
[],
[],
[{}, {}, {}, {}, {}, {"sizes": [0.5], "mean": 0.5, "std_dev": 0.1}],
]
region = get_region_from_grid(frame_shape, box, 320, region_grid)
assert region[2] - region[0] > 320

View File

@ -77,7 +77,7 @@ class NorfairTracker(ObjectTracker):
self.tracker = Tracker( self.tracker = Tracker(
distance_function=frigate_distance, distance_function=frigate_distance,
distance_threshold=2.5, distance_threshold=2.5,
initialization_delay=config.detect.fps / 2, initialization_delay=0,
hit_counter_max=self.max_disappeared, hit_counter_max=self.max_disappeared,
) )
if self.ptz_autotracker_enabled.value: if self.ptz_autotracker_enabled.value:
@ -106,6 +106,11 @@ class NorfairTracker(ObjectTracker):
"ymax": self.detect_config.height, "ymax": self.detect_config.height,
} }
# start object with a hit count of `fps` to avoid quick detection -> loss
next(
(o for o in self.tracker.tracked_objects if o.global_id == track_id)
).hit_counter = self.camera_config.detect.fps
def deregister(self, id, track_id): def deregister(self, id, track_id):
del self.tracked_objects[id] del self.tracked_objects[id]
del self.disappeared[id] del self.disappeared[id]

View File

@ -14,6 +14,7 @@ import numpy as np
import pytz import pytz
import yaml import yaml
from ruamel.yaml import YAML from ruamel.yaml import YAML
from tzlocal import get_localzone
from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
@ -262,3 +263,10 @@ def find_by_key(dictionary, target_key):
if result is not None: if result is not None:
return result return result
return None return None
def get_tomorrow_at_2() -> datetime.datetime:
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
return tomorrow.replace(hour=2, minute=0, second=0).astimezone(
datetime.timezone.utc
)

485
frigate/util/object.py Normal file
View File

@ -0,0 +1,485 @@
"""Utils for reading and writing object detection data."""
import datetime
import logging
import math
import cv2
import numpy as np
from peewee import DoesNotExist
from frigate.config import DetectConfig, ModelConfig
from frigate.const import LABEL_CONSOLIDATION_DEFAULT, LABEL_CONSOLIDATION_MAP
from frigate.detectors.detector_config import PixelFormatEnum
from frigate.models import Event, Regions, Timeline
from frigate.util.image import (
area,
calculate_region,
intersection,
intersection_over_union,
yuv_region_2_bgr,
yuv_region_2_rgb,
yuv_region_2_yuv,
)
logger = logging.getLogger(__name__)
GRID_SIZE = 8
def get_camera_regions_grid(
name: str, detect: DetectConfig
) -> list[list[dict[str, any]]]:
"""Build a grid of expected region sizes for a camera."""
# get grid from db if available
try:
regions: Regions = Regions.select().where(Regions.camera == name).get()
grid = regions.grid
last_update = regions.last_update
except DoesNotExist:
grid = []
for x in range(GRID_SIZE):
row = []
for y in range(GRID_SIZE):
row.append({"sizes": []})
grid.append(row)
last_update = 0
# get events for timeline entries
events = (
Event.select(Event.id)
.where(Event.camera == name)
.where((Event.false_positive == None) | (Event.false_positive == False))
.where(Event.start_time > last_update)
)
valid_event_ids = [e["id"] for e in events.dicts()]
logger.debug(f"Found {len(valid_event_ids)} new events for {name}")
# no new events, return as is
if not valid_event_ids:
return grid
new_update = datetime.datetime.now().timestamp()
timeline = (
Timeline.select(
*[
Timeline.camera,
Timeline.source,
Timeline.data,
]
)
.where(Timeline.source_id << valid_event_ids)
.limit(10000)
.dicts()
)
logger.debug(f"Found {len(timeline)} new entries for {name}")
width = detect.width
height = detect.height
for t in timeline:
if t.get("source") != "tracked_object":
continue
box = t["data"]["box"]
# calculate centroid position
x = box[0] + (box[2] / 2)
y = box[1] + (box[3] / 2)
x_pos = int(x * GRID_SIZE)
y_pos = int(y * GRID_SIZE)
calculated_region = calculate_region(
(height, width),
box[0] * width,
box[1] * height,
(box[0] + box[2]) * width,
(box[1] + box[3]) * height,
320,
1.35,
)
# save width of region to grid as relative
grid[x_pos][y_pos]["sizes"].append(
(calculated_region[2] - calculated_region[0]) / width
)
for x in range(GRID_SIZE):
for y in range(GRID_SIZE):
cell = grid[x][y]
if len(cell["sizes"]) == 0:
continue
std_dev = np.std(cell["sizes"])
mean = np.mean(cell["sizes"])
logger.debug(f"std dev: {std_dev} mean: {mean}")
cell["x"] = x
cell["y"] = y
cell["std_dev"] = std_dev
cell["mean"] = mean
# update db with new grid
region = {
Regions.camera: name,
Regions.grid: grid,
Regions.last_update: new_update,
}
(
Regions.insert(region)
.on_conflict(
conflict_target=[Regions.camera],
update=region,
)
.execute()
)
return grid
def get_cluster_region_from_grid(frame_shape, min_region, cluster, boxes, region_grid):
min_x = frame_shape[1]
min_y = frame_shape[0]
max_x = 0
max_y = 0
for b in cluster:
min_x = min(boxes[b][0], min_x)
min_y = min(boxes[b][1], min_y)
max_x = max(boxes[b][2], max_x)
max_y = max(boxes[b][3], max_y)
return get_region_from_grid(
frame_shape, [min_x, min_y, max_x, max_y], min_region, region_grid
)
def get_region_from_grid(
frame_shape: tuple[int],
cluster: list[int],
min_region: int,
region_grid: list[list[dict[str, any]]],
) -> list[int]:
"""Get a region for a box based on the region grid."""
box = calculate_region(
frame_shape, cluster[0], cluster[1], cluster[2], cluster[3], min_region
)
centroid = (
box[0] + (min(frame_shape[1], box[2]) - box[0]) / 2,
box[1] + (min(frame_shape[0], box[3]) - box[1]) / 2,
)
grid_x = int(centroid[0] / frame_shape[1] * GRID_SIZE)
grid_y = int(centroid[1] / frame_shape[0] * GRID_SIZE)
cell = region_grid[grid_x][grid_y]
# if there is no known data, get standard region for motion box
if not cell or not cell["sizes"]:
return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region)
# convert the calculated region size to relative
calc_size = (box[2] - box[0]) / frame_shape[1]
# if region is within expected size, don't resize
if (
(cell["mean"] - cell["std_dev"])
<= calc_size
<= (cell["mean"] + cell["std_dev"])
):
return box
# TODO not sure how to handle case where cluster is larger than expected region
elif calc_size > (cell["mean"] + cell["std_dev"]):
return box
size = cell["mean"] * frame_shape[1]
# get region based on grid size
return calculate_region(
frame_shape,
max(0, centroid[0] - size / 2),
max(0, centroid[1] - size / 2),
min(frame_shape[1], centroid[0] + size / 2),
min(frame_shape[0], centroid[1] + size / 2),
min_region,
)
def is_object_filtered(obj, objects_to_track, object_filters):
object_name = obj[0]
object_score = obj[1]
object_box = obj[2]
object_area = obj[3]
object_ratio = obj[4]
if object_name not in objects_to_track:
return True
if object_name in object_filters:
obj_settings = object_filters[object_name]
# if the min area is larger than the
# detected object, don't add it to detected objects
if obj_settings.min_area > object_area:
return True
# if the detected object is larger than the
# max area, don't add it to detected objects
if obj_settings.max_area < object_area:
return True
# if the score is lower than the min_score, skip
if obj_settings.min_score > object_score:
return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > object_ratio:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < object_ratio:
return True
if obj_settings.mask is not None:
# compute the coordinates of the object and make sure
# the location isn't outside the bounds of the image (can happen from rounding)
object_xmin = object_box[0]
object_xmax = object_box[2]
object_ymax = object_box[3]
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
x_location = min(
int((object_xmax + object_xmin) / 2.0),
len(obj_settings.mask[0]) - 1,
)
# if the object is in a masked location, don't add it to detected objects
if obj_settings.mask[y_location][x_location] == 0:
return True
return False
def get_min_region_size(model_config: ModelConfig) -> int:
"""Get the min region size."""
return max(model_config.height, model_config.width)
def create_tensor_input(frame, model_config: ModelConfig, region):
if model_config.input_pixel_format == PixelFormatEnum.rgb:
cropped_frame = yuv_region_2_rgb(frame, region)
elif model_config.input_pixel_format == PixelFormatEnum.bgr:
cropped_frame = yuv_region_2_bgr(frame, region)
else:
cropped_frame = yuv_region_2_yuv(frame, region)
# Resize if needed
if cropped_frame.shape != (model_config.height, model_config.width, 3):
cropped_frame = cv2.resize(
cropped_frame,
dsize=(model_config.width, model_config.height),
interpolation=cv2.INTER_LINEAR,
)
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
return np.expand_dims(cropped_frame, axis=0)
def box_overlaps(b1, b2):
if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
return False
return True
def box_inside(b1, b2):
# check if b2 is inside b1
if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]:
return True
return False
def reduce_boxes(boxes, iou_threshold=0.0):
clusters = []
for box in boxes:
matched = 0
for cluster in clusters:
if intersection_over_union(box, cluster) > iou_threshold:
matched = 1
cluster[0] = min(cluster[0], box[0])
cluster[1] = min(cluster[1], box[1])
cluster[2] = max(cluster[2], box[2])
cluster[3] = max(cluster[3], box[3])
if not matched:
clusters.append(list(box))
return [tuple(c) for c in clusters]
def intersects_any(box_a, boxes):
for box in boxes:
if box_overlaps(box_a, box):
return True
return False
def inside_any(box_a, boxes):
for box in boxes:
# check if box_a is inside of box
if box_inside(box, box_a):
return True
return False
def get_cluster_boundary(box, min_region):
# compute the max region size for the current box (box is 10% of region)
box_width = box[2] - box[0]
box_height = box[3] - box[1]
max_region_area = abs(box_width * box_height) / 0.1
max_region_size = max(min_region, int(math.sqrt(max_region_area)))
centroid = (box_width / 2 + box[0], box_height / 2 + box[1])
max_x_dist = int(max_region_size - box_width / 2 * 1.1)
max_y_dist = int(max_region_size - box_height / 2 * 1.1)
return [
int(centroid[0] - max_x_dist),
int(centroid[1] - max_y_dist),
int(centroid[0] + max_x_dist),
int(centroid[1] + max_y_dist),
]
def get_cluster_candidates(frame_shape, min_region, boxes):
# and create a cluster of other boxes using it's max region size
# only include boxes where the region is an appropriate(except the region could possibly be smaller?)
# size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset
# determined by the max_region size minus half the box + 20%
# TODO: see if we can do this with numpy
cluster_candidates = []
used_boxes = []
# loop over each box
for current_index, b in enumerate(boxes):
if current_index in used_boxes:
continue
cluster = [current_index]
used_boxes.append(current_index)
cluster_boundary = get_cluster_boundary(b, min_region)
# find all other boxes that fit inside the boundary
for compare_index, compare_box in enumerate(boxes):
if compare_index in used_boxes:
continue
# if the box is not inside the potential cluster area, cluster them
if not box_inside(cluster_boundary, compare_box):
continue
# get the region if you were to add this box to the cluster
potential_cluster = cluster + [compare_index]
cluster_region = get_cluster_region(
frame_shape, min_region, potential_cluster, boxes
)
# if region could be smaller and either box would be too small
# for the resulting region, dont cluster
should_cluster = True
if (cluster_region[2] - cluster_region[0]) > min_region:
for b in potential_cluster:
box = boxes[b]
# boxes should be more than 5% of the area of the region
if area(box) / area(cluster_region) < 0.05:
should_cluster = False
break
if should_cluster:
cluster.append(compare_index)
used_boxes.append(compare_index)
cluster_candidates.append(cluster)
# return the unique clusters only
unique = {tuple(sorted(c)) for c in cluster_candidates}
return [list(tup) for tup in unique]
def get_cluster_region(frame_shape, min_region, cluster, boxes):
min_x = frame_shape[1]
min_y = frame_shape[0]
max_x = 0
max_y = 0
for b in cluster:
min_x = min(boxes[b][0], min_x)
min_y = min(boxes[b][1], min_y)
max_x = max(boxes[b][2], max_x)
max_y = max(boxes[b][3], max_y)
return calculate_region(
frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.2
)
def get_consolidated_object_detections(detected_object_groups):
"""Drop detections that overlap too much"""
consolidated_detections = []
for group in detected_object_groups.values():
# if the group only has 1 item, skip
if len(group) == 1:
consolidated_detections.append(group[0])
continue
# sort smallest to largest by area
sorted_by_area = sorted(group, key=lambda g: g[3])
for current_detection_idx in range(0, len(sorted_by_area)):
current_detection = sorted_by_area[current_detection_idx]
current_label = current_detection[0]
current_box = current_detection[2]
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
len(sorted_by_area),
):
to_check = sorted_by_area[to_check_idx][2]
intersect_box = intersection(current_box, to_check)
# if 90% of smaller detection is inside of another detection, consolidate
if intersect_box is not None and area(intersect_box) / area(
current_box
) > LABEL_CONSOLIDATION_MAP.get(
current_label, LABEL_CONSOLIDATION_DEFAULT
):
overlap = 1
break
if overlap == 0:
consolidated_detections.append(sorted_by_area[current_detection_idx])
return consolidated_detections
def get_startup_regions(
frame_shape: tuple[int],
region_min_size: int,
region_grid: list[list[dict[str, any]]],
) -> list[list[int]]:
"""Get a list of regions to run on startup."""
# return 8 most popular regions for the camera
all_cells = np.concatenate(region_grid).flat
startup_cells = sorted(all_cells, key=lambda c: len(c["sizes"]), reverse=True)[0:8]
regions = []
for cell in startup_cells:
# rest of the cells are empty
if not cell["sizes"]:
break
x = frame_shape[1] / GRID_SIZE * (0.5 + cell["x"])
y = frame_shape[0] / GRID_SIZE * (0.5 + cell["y"])
size = cell["mean"] * frame_shape[1]
regions.append(
calculate_region(
frame_shape,
x - size / 2,
y - size / 2,
x + size / 2,
y + size / 2,
region_min_size,
multiplier=1,
)
)
return regions

View File

@ -1,6 +1,5 @@
import datetime import datetime
import logging import logging
import math
import multiprocessing as mp import multiprocessing as mp
import os import os
import queue import queue
@ -15,8 +14,12 @@ import numpy as np
from setproctitle import setproctitle from setproctitle import setproctitle
from frigate.config import CameraConfig, DetectConfig, ModelConfig from frigate.config import CameraConfig, DetectConfig, ModelConfig
from frigate.const import ALL_ATTRIBUTE_LABELS, ATTRIBUTE_LABEL_MAP, CACHE_DIR from frigate.const import (
from frigate.detectors.detector_config import PixelFormatEnum ALL_ATTRIBUTE_LABELS,
ATTRIBUTE_LABEL_MAP,
CACHE_DIR,
REQUEST_REGION_GRID,
)
from frigate.log import LogPipe from frigate.log import LogPipe
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
from frigate.motion.improved_motion import ImprovedMotionDetector from frigate.motion.improved_motion import ImprovedMotionDetector
@ -24,103 +27,30 @@ from frigate.object_detection import RemoteObjectDetector
from frigate.track import ObjectTracker from frigate.track import ObjectTracker
from frigate.track.norfair_tracker import NorfairTracker from frigate.track.norfair_tracker import NorfairTracker
from frigate.types import PTZMetricsTypes from frigate.types import PTZMetricsTypes
from frigate.util.builtin import EventsPerSecond from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_2
from frigate.util.image import ( from frigate.util.image import (
FrameManager, FrameManager,
SharedMemoryFrameManager, SharedMemoryFrameManager,
area,
calculate_region,
draw_box_with_label, draw_box_with_label,
intersection, )
intersection_over_union, from frigate.util.object import (
yuv_region_2_bgr, box_inside,
yuv_region_2_rgb, create_tensor_input,
yuv_region_2_yuv, get_cluster_candidates,
get_cluster_region,
get_cluster_region_from_grid,
get_consolidated_object_detections,
get_min_region_size,
get_startup_regions,
inside_any,
intersects_any,
is_object_filtered,
) )
from frigate.util.services import listen from frigate.util.services import listen
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def filtered(obj, objects_to_track, object_filters):
object_name = obj[0]
object_score = obj[1]
object_box = obj[2]
object_area = obj[3]
object_ratio = obj[4]
if object_name not in objects_to_track:
return True
if object_name in object_filters:
obj_settings = object_filters[object_name]
# if the min area is larger than the
# detected object, don't add it to detected objects
if obj_settings.min_area > object_area:
return True
# if the detected object is larger than the
# max area, don't add it to detected objects
if obj_settings.max_area < object_area:
return True
# if the score is lower than the min_score, skip
if obj_settings.min_score > object_score:
return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > object_ratio:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < object_ratio:
return True
if obj_settings.mask is not None:
# compute the coordinates of the object and make sure
# the location isn't outside the bounds of the image (can happen from rounding)
object_xmin = object_box[0]
object_xmax = object_box[2]
object_ymax = object_box[3]
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
x_location = min(
int((object_xmax + object_xmin) / 2.0),
len(obj_settings.mask[0]) - 1,
)
# if the object is in a masked location, don't add it to detected objects
if obj_settings.mask[y_location][x_location] == 0:
return True
return False
def get_min_region_size(model_config: ModelConfig) -> int:
"""Get the min region size."""
return max(model_config.height, model_config.width)
def create_tensor_input(frame, model_config: ModelConfig, region):
if model_config.input_pixel_format == PixelFormatEnum.rgb:
cropped_frame = yuv_region_2_rgb(frame, region)
elif model_config.input_pixel_format == PixelFormatEnum.bgr:
cropped_frame = yuv_region_2_bgr(frame, region)
else:
cropped_frame = yuv_region_2_yuv(frame, region)
# Resize if needed
if cropped_frame.shape != (model_config.height, model_config.width, 3):
cropped_frame = cv2.resize(
cropped_frame,
dsize=(model_config.width, model_config.height),
interpolation=cv2.INTER_LINEAR,
)
# Expand dimensions since the model expects images to have shape: [1, height, width, 3]
return np.expand_dims(cropped_frame, axis=0)
def stop_ffmpeg(ffmpeg_process, logger): def stop_ffmpeg(ffmpeg_process, logger):
logger.info("Terminating the existing ffmpeg process...") logger.info("Terminating the existing ffmpeg process...")
ffmpeg_process.terminate() ffmpeg_process.terminate()
@ -455,8 +385,10 @@ def track_camera(
detection_queue, detection_queue,
result_connection, result_connection,
detected_objects_queue, detected_objects_queue,
inter_process_queue,
process_info, process_info,
ptz_metrics, ptz_metrics,
region_grid,
): ):
stop_event = mp.Event() stop_event = mp.Event()
@ -471,6 +403,7 @@ def track_camera(
listen() listen()
frame_queue = process_info["frame_queue"] frame_queue = process_info["frame_queue"]
region_grid_queue = process_info["region_grid_queue"]
detection_enabled = process_info["detection_enabled"] detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"] motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"] improve_contrast_enabled = process_info["improve_contrast_enabled"]
@ -499,7 +432,9 @@ def track_camera(
process_frames( process_frames(
name, name,
inter_process_queue,
frame_queue, frame_queue,
region_grid_queue,
frame_shape, frame_shape,
model_config, model_config,
config.detect, config.detect,
@ -515,50 +450,12 @@ def track_camera(
motion_enabled, motion_enabled,
stop_event, stop_event,
ptz_metrics, ptz_metrics,
region_grid,
) )
logger.info(f"{name}: exiting subprocess") logger.info(f"{name}: exiting subprocess")
def box_overlaps(b1, b2):
if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
return False
return True
def box_inside(b1, b2):
# check if b2 is inside b1
if b2[0] >= b1[0] and b2[1] >= b1[1] and b2[2] <= b1[2] and b2[3] <= b1[3]:
return True
return False
def reduce_boxes(boxes, iou_threshold=0.0):
clusters = []
for box in boxes:
matched = 0
for cluster in clusters:
if intersection_over_union(box, cluster) > iou_threshold:
matched = 1
cluster[0] = min(cluster[0], box[0])
cluster[1] = min(cluster[1], box[1])
cluster[2] = max(cluster[2], box[2])
cluster[3] = max(cluster[3], box[3])
if not matched:
clusters.append(list(box))
return [tuple(c) for c in clusters]
def intersects_any(box_a, boxes):
for box in boxes:
if box_overlaps(box_a, box):
return True
return False
def detect( def detect(
detect_config: DetectConfig, detect_config: DetectConfig,
object_detector, object_detector,
@ -597,134 +494,17 @@ def detect(
region, region,
) )
# apply object filters # apply object filters
if filtered(det, objects_to_track, object_filters): if is_object_filtered(det, objects_to_track, object_filters):
continue continue
detections.append(det) detections.append(det)
return detections return detections
def get_cluster_boundary(box, min_region):
# compute the max region size for the current box (box is 10% of region)
box_width = box[2] - box[0]
box_height = box[3] - box[1]
max_region_area = abs(box_width * box_height) / 0.1
max_region_size = max(min_region, int(math.sqrt(max_region_area)))
centroid = (box_width / 2 + box[0], box_height / 2 + box[1])
max_x_dist = int(max_region_size - box_width / 2 * 1.1)
max_y_dist = int(max_region_size - box_height / 2 * 1.1)
return [
int(centroid[0] - max_x_dist),
int(centroid[1] - max_y_dist),
int(centroid[0] + max_x_dist),
int(centroid[1] + max_y_dist),
]
def get_cluster_candidates(frame_shape, min_region, boxes):
# and create a cluster of other boxes using it's max region size
# only include boxes where the region is an appropriate(except the region could possibly be smaller?)
# size in the cluster. in order to be in the cluster, the furthest corner needs to be within x,y offset
# determined by the max_region size minus half the box + 20%
# TODO: see if we can do this with numpy
cluster_candidates = []
used_boxes = []
# loop over each box
for current_index, b in enumerate(boxes):
if current_index in used_boxes:
continue
cluster = [current_index]
used_boxes.append(current_index)
cluster_boundary = get_cluster_boundary(b, min_region)
# find all other boxes that fit inside the boundary
for compare_index, compare_box in enumerate(boxes):
if compare_index in used_boxes:
continue
# if the box is not inside the potential cluster area, cluster them
if not box_inside(cluster_boundary, compare_box):
continue
# get the region if you were to add this box to the cluster
potential_cluster = cluster + [compare_index]
cluster_region = get_cluster_region(
frame_shape, min_region, potential_cluster, boxes
)
# if region could be smaller and either box would be too small
# for the resulting region, dont cluster
should_cluster = True
if (cluster_region[2] - cluster_region[0]) > min_region:
for b in potential_cluster:
box = boxes[b]
# boxes should be more than 5% of the area of the region
if area(box) / area(cluster_region) < 0.05:
should_cluster = False
break
if should_cluster:
cluster.append(compare_index)
used_boxes.append(compare_index)
cluster_candidates.append(cluster)
# return the unique clusters only
unique = {tuple(sorted(c)) for c in cluster_candidates}
return [list(tup) for tup in unique]
def get_cluster_region(frame_shape, min_region, cluster, boxes):
min_x = frame_shape[1]
min_y = frame_shape[0]
max_x = 0
max_y = 0
for b in cluster:
min_x = min(boxes[b][0], min_x)
min_y = min(boxes[b][1], min_y)
max_x = max(boxes[b][2], max_x)
max_y = max(boxes[b][3], max_y)
return calculate_region(
frame_shape, min_x, min_y, max_x, max_y, min_region, multiplier=1.2
)
def get_consolidated_object_detections(detected_object_groups):
"""Drop detections that overlap too much"""
consolidated_detections = []
for group in detected_object_groups.values():
# if the group only has 1 item, skip
if len(group) == 1:
consolidated_detections.append(group[0])
continue
# sort smallest to largest by area
sorted_by_area = sorted(group, key=lambda g: g[3])
for current_detection_idx in range(0, len(sorted_by_area)):
current_detection = sorted_by_area[current_detection_idx][2]
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
len(sorted_by_area),
):
to_check = sorted_by_area[to_check_idx][2]
intersect_box = intersection(current_detection, to_check)
# if 90% of smaller detection is inside of another detection, consolidate
if (
intersect_box is not None
and area(intersect_box) / area(current_detection) > 0.9
):
overlap = 1
break
if overlap == 0:
consolidated_detections.append(sorted_by_area[current_detection_idx])
return consolidated_detections
def process_frames( def process_frames(
camera_name: str, camera_name: str,
inter_process_queue: mp.Queue,
frame_queue: mp.Queue, frame_queue: mp.Queue,
region_grid_queue: mp.Queue,
frame_shape, frame_shape,
model_config: ModelConfig, model_config: ModelConfig,
detect_config: DetectConfig, detect_config: DetectConfig,
@ -740,20 +520,36 @@ def process_frames(
motion_enabled: mp.Value, motion_enabled: mp.Value,
stop_event, stop_event,
ptz_metrics: PTZMetricsTypes, ptz_metrics: PTZMetricsTypes,
region_grid,
exit_on_empty: bool = False, exit_on_empty: bool = False,
): ):
fps = process_info["process_fps"] fps = process_info["process_fps"]
detection_fps = process_info["detection_fps"] detection_fps = process_info["detection_fps"]
current_frame_time = process_info["detection_frame"] current_frame_time = process_info["detection_frame"]
next_region_update = get_tomorrow_at_2()
fps_tracker = EventsPerSecond() fps_tracker = EventsPerSecond()
fps_tracker.start() fps_tracker.start()
startup_scan_counter = 0 startup_scan = True
stationary_frame_counter = 0
region_min_size = get_min_region_size(model_config) region_min_size = get_min_region_size(model_config)
while not stop_event.is_set(): while not stop_event.is_set():
if (
datetime.datetime.now().astimezone(datetime.timezone.utc)
> next_region_update
):
inter_process_queue.put((REQUEST_REGION_GRID, camera_name))
try:
region_grid = region_grid_queue.get(True, 10)
except queue.Empty:
logger.error(f"Unable to get updated region grid for {camera_name}")
next_region_update = get_tomorrow_at_2()
try: try:
if exit_on_empty: if exit_on_empty:
frame_time = frame_queue.get(False) frame_time = frame_queue.get(False)
@ -790,65 +586,79 @@ def process_frames(
# check every Nth frame for stationary objects # check every Nth frame for stationary objects
# disappeared objects are not stationary # disappeared objects are not stationary
# also check for overlapping motion boxes # also check for overlapping motion boxes
if stationary_frame_counter == detect_config.stationary.interval:
stationary_frame_counter = 0
stationary_object_ids = []
else:
stationary_frame_counter += 1
stationary_object_ids = [ stationary_object_ids = [
obj["id"] obj["id"]
for obj in object_tracker.tracked_objects.values() for obj in object_tracker.tracked_objects.values()
# if it has exceeded the stationary threshold # if it has exceeded the stationary threshold
if obj["motionless_count"] >= detect_config.stationary.threshold if obj["motionless_count"] >= detect_config.stationary.threshold
# and it isn't due for a periodic check
and (
detect_config.stationary.interval == 0
or obj["motionless_count"] % detect_config.stationary.interval != 0
)
# and it hasn't disappeared # and it hasn't disappeared
and object_tracker.disappeared[obj["id"]] == 0 and object_tracker.disappeared[obj["id"]] == 0
# and it doesn't overlap with any current motion boxes when not calibrating # and it doesn't overlap with any current motion boxes when not calibrating
and not intersects_any( and not intersects_any(
obj["box"], [] if motion_detector.is_calibrating() else motion_boxes obj["box"],
[] if motion_detector.is_calibrating() else motion_boxes,
) )
] ]
# get tracked object boxes that aren't stationary # get tracked object boxes that aren't stationary
tracked_object_boxes = [ tracked_object_boxes = [
(
# use existing object box for stationary objects
obj["estimate"] obj["estimate"]
if obj["motionless_count"] < detect_config.stationary.threshold
else obj["box"]
)
for obj in object_tracker.tracked_objects.values() for obj in object_tracker.tracked_objects.values()
if obj["id"] not in stationary_object_ids if obj["id"] not in stationary_object_ids
] ]
combined_boxes = tracked_object_boxes # get consolidated regions for tracked objects
# only add in the motion boxes when not calibrating
if not motion_detector.is_calibrating():
combined_boxes += motion_boxes
cluster_candidates = get_cluster_candidates(
frame_shape, region_min_size, combined_boxes
)
regions = [ regions = [
get_cluster_region( get_cluster_region(
frame_shape, region_min_size, candidate, combined_boxes frame_shape, region_min_size, candidate, tracked_object_boxes
)
for candidate in get_cluster_candidates(
frame_shape, region_min_size, tracked_object_boxes
) )
for candidate in cluster_candidates
] ]
# if starting up, get the next startup scan region # only add in the motion boxes when not calibrating
if startup_scan_counter < 9: if not motion_detector.is_calibrating():
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3) # find motion boxes that are not inside tracked object regions
ymax = int(frame_shape[0] / 3 + ymin) standalone_motion_boxes = [
xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3) b for b in motion_boxes if not inside_any(b, regions)
xmax = int(frame_shape[1] / 3 + xmin) ]
regions.append(
calculate_region( if standalone_motion_boxes:
motion_clusters = get_cluster_candidates(
frame_shape, frame_shape,
xmin,
ymin,
xmax,
ymax,
region_min_size, region_min_size,
multiplier=1.2, standalone_motion_boxes,
) )
motion_regions = [
get_cluster_region_from_grid(
frame_shape,
region_min_size,
candidate,
standalone_motion_boxes,
region_grid,
) )
startup_scan_counter += 1 for candidate in motion_clusters
]
regions += motion_regions
# if starting up, get the next startup scan region
if startup_scan:
for region in get_startup_regions(
frame_shape, region_min_size, region_grid
):
regions.append(region)
startup_scan = False
# resize regions and detect # resize regions and detect
# seed with stationary objects # seed with stationary objects

View File

@ -0,0 +1,35 @@
"""Peewee migrations -- 019_create_regions_table.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import peewee as pw
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.sql(
'CREATE TABLE IF NOT EXISTS "regions" ("camera" VARCHAR(20) NOT NULL PRIMARY KEY, "last_update" DATETIME NOT NULL, "grid" JSON)'
)
def rollback(migrator, database, fake=False, **kwargs):
pass

266
web/package-lock.json generated
View File

@ -978,21 +978,21 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.51.0", "version": "8.52.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz",
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.11", "version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^1.2.1", "@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
}, },
@ -1014,9 +1014,9 @@
} }
}, },
"node_modules/@humanwhocodes/object-schema": { "node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true "dev": true
}, },
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
@ -1664,16 +1664,16 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
"integrity": "sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==", "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/type-utils": "6.7.5", "@typescript-eslint/type-utils": "6.8.0",
"@typescript-eslint/utils": "6.7.5", "@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@ -1870,15 +1870,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
"integrity": "sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==", "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -1898,13 +1898,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
"integrity": "sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==", "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5" "@typescript-eslint/visitor-keys": "6.8.0"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@ -1915,13 +1915,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
"integrity": "sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==", "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/utils": "6.7.5", "@typescript-eslint/utils": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
}, },
@ -1942,9 +1942,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
"integrity": "sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==", "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@ -1955,13 +1955,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
"integrity": "sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==", "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -1997,17 +1997,17 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==", "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"engines": { "engines": {
@ -2037,12 +2037,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
"integrity": "sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==", "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
"engines": { "engines": {
@ -2053,6 +2053,12 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"node_modules/@videojs/http-streaming": { "node_modules/@videojs/http-streaming": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz", "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
@ -3677,18 +3683,19 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.51.0", "version": "8.52.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2", "@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.51.0", "@eslint/js": "8.52.0",
"@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -4071,9 +4078,9 @@
} }
}, },
"node_modules/eslint-plugin-jest": { "node_modules/eslint-plugin-jest": {
"version": "27.4.2", "version": "27.4.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
"integrity": "sha512-3Nfvv3wbq2+PZlRTf2oaAWXWwbdBejFRBR2O8tAO67o+P8zno+QGbcDYaAXODlreXVg+9gvWhKKmG2rgfb8GEg==", "integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/utils": "^5.10.0" "@typescript-eslint/utils": "^5.10.0"
@ -9223,9 +9230,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.11", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@ -10291,18 +10298,18 @@
} }
}, },
"@eslint/js": { "@eslint/js": {
"version": "8.51.0", "version": "8.52.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz",
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
"dev": true "dev": true
}, },
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.11.11", "version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@humanwhocodes/object-schema": "^1.2.1", "@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
} }
@ -10314,9 +10321,9 @@
"dev": true "dev": true
}, },
"@humanwhocodes/object-schema": { "@humanwhocodes/object-schema": {
"version": "1.2.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true "dev": true
}, },
"@istanbuljs/schema": { "@istanbuljs/schema": {
@ -10830,16 +10837,16 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
"integrity": "sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==", "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/type-utils": "6.7.5", "@typescript-eslint/type-utils": "6.8.0",
"@typescript-eslint/utils": "6.7.5", "@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@ -10953,54 +10960,54 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
"integrity": "sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==", "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4" "debug": "^4.3.4"
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
"integrity": "sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==", "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5" "@typescript-eslint/visitor-keys": "6.8.0"
} }
}, },
"@typescript-eslint/type-utils": { "@typescript-eslint/type-utils": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
"integrity": "sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==", "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/utils": "6.7.5", "@typescript-eslint/utils": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
"integrity": "sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==", "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
"integrity": "sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==", "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.7.5", "@typescript-eslint/visitor-keys": "6.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -11020,17 +11027,17 @@
} }
}, },
"@typescript-eslint/utils": { "@typescript-eslint/utils": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==", "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.7.5", "@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.7.5", "@typescript-eslint/typescript-estree": "6.8.0",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"dependencies": { "dependencies": {
@ -11046,15 +11053,21 @@
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "6.7.5", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
"integrity": "sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==", "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "6.7.5", "@typescript-eslint/types": "6.8.0",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
} }
}, },
"@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
"@videojs/http-streaming": { "@videojs/http-streaming": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz", "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
@ -12259,18 +12272,19 @@
"dev": true "dev": true
}, },
"eslint": { "eslint": {
"version": "8.51.0", "version": "8.52.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2", "@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.51.0", "@eslint/js": "8.52.0",
"@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -12584,9 +12598,9 @@
} }
}, },
"eslint-plugin-jest": { "eslint-plugin-jest": {
"version": "27.4.2", "version": "27.4.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
"integrity": "sha512-3Nfvv3wbq2+PZlRTf2oaAWXWwbdBejFRBR2O8tAO67o+P8zno+QGbcDYaAXODlreXVg+9gvWhKKmG2rgfb8GEg==", "integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/utils": "^5.10.0" "@typescript-eslint/utils": "^5.10.0"
@ -16240,9 +16254,9 @@
} }
}, },
"vite": { "vite": {
"version": "4.4.11", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true, "dev": true,
"requires": { "requires": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",

View File

@ -21,7 +21,7 @@ export default function LargeDialog({ children, portalRootID = 'dialogs' }) {
> >
<div <div
role="modal" role="modal"
className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 w-4/5 md:h-2/3 max-w-7xl text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${ className={`absolute rounded shadow-2xl bg-white w-full max-h-fit sm:max-w-md md:max-w-lg lg:max-w-xl xl:max-w-2xl dark:bg-gray-700 text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
show ? 'scale-100 opacity-100' : '' show ? 'scale-100 opacity-100' : ''
}`} }`}
> >

View File

@ -1,10 +1,11 @@
import { h } from 'preact'; import { h } from 'preact';
import { baseUrl } from '../api/baseUrl'; import { baseUrl } from '../api/baseUrl';
import { useCallback, useEffect } from 'preact/hooks'; import { useCallback, useEffect } from 'preact/hooks';
import { useMemo } from 'react';
export default function WebRtcPlayer({ camera, width, height }) { export default function WebRtcPlayer({ camera, width, height }) {
const url = `${baseUrl.replace(/^http/, 'ws')}live/webrtc/api/ws?src=${camera}`; const url = `${baseUrl.replace(/^http/, 'ws')}live/webrtc/api/ws?src=${camera}`;
const ws = useMemo(() => new WebSocket(url), [url])
const PeerConnection = useCallback(async (media) => { const PeerConnection = useCallback(async (media) => {
const pc = new RTCPeerConnection({ const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
@ -60,7 +61,6 @@ export default function WebRtcPlayer({ camera, width, height }) {
const connect = useCallback(async () => { const connect = useCallback(async () => {
const pc = await PeerConnection('video+audio'); const pc = await PeerConnection('video+audio');
const ws = new WebSocket(url);
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
pc.addEventListener('icecandidate', (ev) => { pc.addEventListener('icecandidate', (ev) => {
@ -85,11 +85,19 @@ export default function WebRtcPlayer({ camera, width, height }) {
pc.setRemoteDescription({ type: 'answer', sdp: msg.value }); pc.setRemoteDescription({ type: 'answer', sdp: msg.value });
} }
}); });
}, [PeerConnection, url]);
ws.addEventListener('close', () => {
pc.close();
})
}, [PeerConnection, ws]);
useEffect(() => { useEffect(() => {
connect(); connect();
}, [connect]);
return () => {
ws.close();
}
}, [connect, ws]);
return ( return (
<div> <div>

View File

@ -94,7 +94,7 @@ export default function Events({ path, ...props }) {
showDeleteFavorite: false, showDeleteFavorite: false,
}); });
const [showInProgress, setShowInProgress] = useState(true); const [showInProgress, setShowInProgress] = useState((props.event || props.cameras || props.labels) == null);
const eventsFetcher = useCallback( const eventsFetcher = useCallback(
(path, params) => { (path, params) => {
@ -121,8 +121,12 @@ export default function Events({ path, ...props }) {
[searchParams] [searchParams]
); );
const { data: ongoingEvents } = useSWR(['events', { in_progress: 1, include_thumbnails: 0 }]); const { data: ongoingEvents, mutate: refreshOngoingEvents } = useSWR(['events', { in_progress: 1, include_thumbnails: 0 }]);
const { data: eventPages, mutate, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher); const { data: eventPages, mutate: refreshEvents, size, setSize, isValidating } = useSWRInfinite(getKey, eventsFetcher);
const mutate = () => {
refreshEvents();
refreshOngoingEvents();
}
const { data: allLabels } = useSWR(['labels']); const { data: allLabels } = useSWR(['labels']);
const { data: allSubLabels } = useSWR(['sub_labels', { split_joined: 1 }]); const { data: allSubLabels } = useSWR(['sub_labels', { split_joined: 1 }]);

View File

@ -32,7 +32,7 @@ export default function System() {
service = {}, service = {},
detection_fps: _, detection_fps: _,
processes, processes,
...cameras cameras,
} = stats || initialStats || emptyObject; } = stats || initialStats || emptyObject;
const detectorNames = Object.keys(detectors || emptyObject); const detectorNames = Object.keys(detectors || emptyObject);