Compare commits

...

24 Commits

Author SHA1 Message Date
Blake Blackshear
65e3e67a83 avoid import error for non-rk builds (#8454)
* avoid import error for non-rk builds

* linter
2023-11-04 07:56:35 -05:00
Nicolas Mowen
63233a5830 Periodically sync for stale recordings (#8433)
* Periodically cleanup recordings files / DB

* Make automatic sync limited ot last 36 hours
2023-11-04 02:21:29 +00:00
Nicolas Mowen
4f7b710112 Don't fail on invalid class IDs for TensorRT detector (#8438)
* Don't fail on invalid class IDs

* Fix whitespace

* Make log warning
2023-11-04 02:19:58 +00:00
coperni
ac53993f70 Add endpoint to restart Frigate (#8440)
* Add endpoint to restart Frigate

The only means of restarting Frigate remotely is to issue
a restart topic on the server's websocket. It's
convenient to also expose this capability via HTTP endpoint.

* Add new section to API docs

* Remove extra line
2023-11-04 02:19:29 +00:00
Josh Hawkins
ef750e73a2 add motion mask recommendation (#8448) 2023-11-04 02:18:43 +00:00
Nicolas Mowen
7270eef6bf Don't fail on 0 rms (#8447) 2023-11-04 02:18:23 +00:00
Marc Altmann
b54aaad382 fix rknn.py (#8434)
Co-authored-by: MarcA711 <>
2023-11-03 00:12:54 +00:00
Josh Hawkins
fc36be4f88 suppress error by overriding class func (#8431) 2023-11-02 23:24:14 +00:00
Blake Blackshear
aefecad4c0 Update deps (#8426)
* update web deps

* other deps
2023-11-02 13:36:49 +00:00
Nicolas Mowen
c57528cbcf Fix rk build (#8430) 2023-11-02 13:36:34 +00:00
Marc Altmann
090294e89b Initial support for rockchip boards (#8382)
* initial support for rockchip boards

* Apply suggestions from code review

apply requested changes

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>

* requested changes

* rewrite dockerfile

* adjust targets

* Update .github/workflows/ci.yml

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>

* Update docs/docs/configuration/object_detectors.md

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>

* Update docs/docs/configuration/object_detectors.md

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>

* add information to docs

* Update docs/docs/configuration/object_detectors.md

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>

* format rknn.py

* apply changes from isort and ruff

---------

Co-authored-by: MarcA711 <>
Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2023-11-02 12:55:24 +00:00
Nicolas Mowen
a6279a0337 Clean up RPi ffmpeg presets (#8428)
* Clean up rpi ffmpeg presets

* Remove from docs

* Put back encoding
2023-11-02 12:54:51 +00:00
Blake Blackshear
0dd3dd23aa add support for docker secrets (#8409)
* add support for docker secrets

* check for directory first
2023-11-02 05:35:30 -05:00
Blake Blackshear
4bd29b2ee8 fix build tag (#8408) 2023-11-02 05:35:19 -05:00
Nicolas Mowen
cc79cbcadc Improve robustness of storage maintenance (#8411)
* Improve robustness of storage maintenance

* Fix tests

* Fix test
2023-11-01 23:21:59 +00:00
Nicolas Mowen
89366d7b12 Add endpoint to return camera frame with regions grid overlaid (#8413)
* Add endpoint to view grid overload on camera frame

* Add api to docs

* Formatting
2023-11-01 23:21:30 +00:00
Josh Hawkins
6eff08eb2d Add MQTT topic for active autotracking (#8419)
* prevent estimate clipping when autotracking

* use unclipped estimate in distance function only

* remove autotracking velocity changes

* publish on init
2023-11-01 23:20:26 +00:00
tpjanssen
8b6b83bd62 Filtering on Frigate+ submits in frontend (#8344)
* Initial draft for filtering Frigate+ submits in frontend

* Hide filter when Frigate+ is not enabled

* Update http.py

* Revert "Update http.py"

This reverts commit fa292682d6.
2023-11-01 23:19:46 +00:00
Nicolas Mowen
8b6e3a0d37 Fix region when no data in grid (#8415)
* Fix region when no data in grid

* Make comment more clear
2023-11-01 23:19:17 +00:00
tpjanssen
8a9b26df4e Visit camera directly from system page (#8405)
* Visit camera directly from system page

* Processed all feedback

* Changed button caption
2023-11-01 07:08:59 -06:00
tpjanssen
fd6a3bd5d2 API recordings snapsnot PNG fix (#8401)
* Update http.py

* Update http.py

Limit query results
2023-11-01 06:14:51 -05:00
Nicolas Mowen
8085ad4b4c Ensure that birdseye error correction uses a resolution that is divisible by 4 (#8398) 2023-11-01 06:13:12 -05:00
Josh Hawkins
af24eb7dbf Autotracking tweaks (#8400)
* optimize motion and velocity estimation

* change recommended fps and fix config validate

* remove unneeded var

* process at most 3 objects per second

* fix test
2023-11-01 06:12:43 -05:00
Blake Blackshear
d1620b4e39 clean passwords when both rtsp and http present (#8399) 2023-10-31 08:04:42 -05:00
48 changed files with 1016 additions and 354 deletions

View File

@@ -79,6 +79,15 @@ jobs:
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
- name: Build and push RockChip build
uses: docker/bake-action@v3
with:
push: true
targets: rk
files: docker/rockchip/rk.hcl
set: |
rk.tags=${{ steps.setup.outputs.image-name }}-rk
*.cache-from=type=gha
jetson_jp4_build:
runs-on: ubuntu-latest
name: Jetson Jetpack 4
@@ -141,7 +150,7 @@ jobs:
- arm64_build
steps:
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v5
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v5
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
@@ -22,8 +22,9 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create tag variables
run: |
BRANCH=$([[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "master" || echo "dev")
echo "BASE=ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}" >> $GITHUB_ENV
echo "BUILD_TAG=${{ github.ref_name }}-${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "BUILD_TAG=${BRANCH}-${GITHUB_SHA::7}" >> $GITHUB_ENV
echo "CLEAN_VERSION=$(echo ${GITHUB_REF##*/} | tr '[:upper:]' '[:lower:]' | sed 's/^[v]//')" >> $GITHUB_ENV
- name: Tag and push the main image
run: |

View File

@@ -2,3 +2,5 @@
/docker/tensorrt/ @madsciencetist @NateMeyer
/docker/tensorrt/*arm64* @madsciencetist
/docker/tensorrt/*jetson* @madsciencetist
/docker/rockchip/ @MarcA711

View File

@@ -13,9 +13,9 @@ psutil == 5.9.*
pydantic == 1.10.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
PyYAML == 6.0.*
pytz == 2023.3
ruamel.yaml == 0.17.*
tzlocal == 5.1
pytz == 2023.3.post1
ruamel.yaml == 0.18.*
tzlocal == 5.2
types-PyYAML == 6.0.*
requests == 2.31.*
types-requests == 2.31.*

View File

@@ -3,6 +3,7 @@
import json
import os
import sys
from pathlib import Path
import yaml
@@ -16,6 +17,14 @@ sys.path.remove("/opt/frigate")
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
for secret_file in os.listdir("/run/secrets"):
if secret_file.startswith("FRIGATE_"):
FRIGATE_ENV_VARS[secret_file] = Path(
os.path.join("/run/secrets", secret_file)
).read_text()
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml

View File

@@ -0,0 +1,24 @@
# syntax=docker/dockerfile:1.6
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND=noninteractive
FROM wheels as rk-wheels
COPY docker/main/requirements-wheels.txt /requirements-wheels.txt
COPY docker/rockchip/requirements-wheels-rk.txt /requirements-wheels-rk.txt
RUN sed -i "/https/d" /requirements-wheels.txt
RUN pip3 wheel --wheel-dir=/rk-wheels -c /requirements-wheels.txt -r /requirements-wheels-rk.txt
FROM wget as rk-libs
RUN wget -qO librknnrt.so https://github.com/MarcA711/rknpu2/raw/master/runtime/RK3588/Linux/librknn_api/aarch64/librknnrt.so
FROM deps AS rk-deps
ARG TARGETARCH
RUN --mount=type=bind,from=rk-wheels,source=/rk-wheels,target=/deps/rk-wheels \
pip3 install -U /deps/rk-wheels/*.whl
WORKDIR /opt/frigate/
COPY --from=rootfs / /
COPY --from=rk-libs /rootfs/librknnrt.so /usr/lib/
COPY docker/rockchip/yolov8n-320x320.rknn /models/

View File

@@ -0,0 +1,2 @@
hide-warnings == 0.17
rknn-toolkit-lite2 @ https://github.com/MarcA711/rknn-toolkit2/raw/master/rknn_toolkit_lite2/packages/rknn_toolkit_lite2-1.5.2-cp39-cp39-linux_aarch64.whl

34
docker/rockchip/rk.hcl Normal file
View File

@@ -0,0 +1,34 @@
target wget {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "wget"
}
target wheels {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "wheels"
}
target deps {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "deps"
}
target rootfs {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "rootfs"
}
target rk {
dockerfile = "docker/rockchip/Dockerfile"
contexts = {
wget = "target:wget",
wheels = "target:wheels",
deps = "target:deps",
rootfs = "target:rootfs"
}
platforms = ["linux/arm64"]
}

10
docker/rockchip/rk.mk Normal file
View File

@@ -0,0 +1,10 @@
BOARDS += rk
local-rk: version
docker buildx bake --load --file=docker/rockchip/rk.hcl --set rk.tags=frigate:latest-rk rk
build-rk: version
docker buildx bake --file=docker/rockchip/rk.hcl --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk rk
push-rk: build-rk
docker buildx bake --push --file=docker/rockchip/rk.hcl --set rk.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-rk rk

Binary file not shown.

View File

@@ -31,7 +31,7 @@ First, set up a PTZ preset in your camera's firmware and give it a name. If you'
Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset.
An [ONVIF connection](cameras.md) is required for autotracking to function.
An [ONVIF connection](cameras.md) is required for autotracking to function. Also, a [motion mask](masks.md) over your camera's timestamp and any overlay text is recommended to ensure they are completely excluded from scene change calculations when the camera is moving.
Note that `autotracking` is disabled by default but can be enabled in the configuration or by MQTT.
@@ -113,7 +113,7 @@ If you initially calibrate with zooming disabled and then enable zooming at a la
Every PTZ camera is different, so autotracking may not perform ideally in every situation. This experimental feature was initially developed using an EmpireTech/Dahua SD1A404XB-GNR.
The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases (especially for faster moving objects), the default 5 fps is insufficient for the motion estimator to perform accurately. 10 fps is the current recommendation. Higher frame rates will likely not be more performant and will only slow down Frigate and the motion estimator. Adjust your camera to output at least 10 frames per second and change the `fps` parameter in the [detect configuration](index.md) of your configuration file.
The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 5 fps is sufficient, but if you plan to track faster moving objects, you may want to increase this slightly. Higher frame rates (> 10fps) will only slow down Frigate and the motion estimator and may lead to dropped frames, especially if you are using experimental zooming.
A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's debug viewer for your camera to see a thicker colored box around the object currently being autotracked.

View File

@@ -13,7 +13,6 @@ See [the hwaccel docs](/configuration/hardware_acceleration.md) for more info on
| Preset | Usage | Other Notes |
| --------------------- | ------------------------------ | ----------------------------------------------------- |
| preset-rpi-32-h264 | 32 bit Rpi with h264 stream | |
| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | |
| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen |
| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead |

View File

@@ -75,11 +75,11 @@ mqtt:
# NOTE: must be unique if you are running multiple instances
client_id: frigate
# Optional: user
# NOTE: MQTT user can be specified with an environment variables that must begin with 'FRIGATE_'.
# NOTE: MQTT user can be specified with an environment variables or docker secrets that must begin with 'FRIGATE_'.
# e.g. user: '{FRIGATE_MQTT_USER}'
user: mqtt_user
# Optional: password
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
# NOTE: MQTT password can be specified with an environment variables or docker secrets that must begin with 'FRIGATE_'.
# e.g. password: '{FRIGATE_MQTT_PASSWORD}'
password: password
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
@@ -491,7 +491,7 @@ cameras:
# Required: A list of input streams for the camera. See documentation for more information.
inputs:
# Required: the path to the stream
# NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {}
# NOTE: path may include environment variables or docker secrets, which must begin with 'FRIGATE_' and be referenced in {}
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
# Required: list of roles for this stream. valid values are: audio,detect,record,rtmp
# NOTICE: In addition to assigning the audio, record, and rtmp roles,
@@ -520,6 +520,9 @@ cameras:
# to be replaced by a newer image. (default: shown below)
best_image_timeout: 60
# Optional: URL to visit the camera web UI directly from the system page. Might not be available on every camera.
webui_url: ""
# Optional: zones for this camera
zones:
# Required: name of the zone

View File

@@ -5,7 +5,7 @@ title: Object Detectors
# Officially Supported Detectors
Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `openvino`, and `tensorrt`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `openvino`, `tensorrt`, and `rknn`. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras.
## CPU Detector (not recommended)
@@ -291,3 +291,38 @@ To verify that the integration is working correctly, start Frigate and observe t
# Community Supported Detectors
## Rockchip RKNN-Toolkit-Lite2
This detector is only available if one of the following Rockchip SoCs is used:
- RK3566/RK3568
- RK3588/RK3588S
- RV1103/RV1106
- RK3562
These SoCs come with a NPU that will highly speed up detection.
### Setup
RKNN support is provided using the `-rk` suffix for the docker image. Moreover, privileged mode must be enabled by adding the `--privileged` flag to your docker run command or `privileged: true` to your `docker-compose.yml` file.
### Configuration
This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for one). Lines that are required at least to use the detector are labeled as required, all other lines are optional.
```yaml
detectors: # required
rknn: # required
type: rknn # required
model: # required
# path to .rknn model file
path: /models/yolov8n-320x320.rknn
# width and height of detection frames
width: 320
height: 320
# pixel format of detection frame
# default value is rgb but yolov models usually use bgr format
input_pixel_format: bgr # required
# shape of detection frame
input_tensor: nhwc
```

View File

@@ -95,6 +95,16 @@ Frigate supports all Jetson boards, from the inexpensive Jetson Nano to the powe
Inference speed will vary depending on the YOLO model, jetson platform and jetson nvpmodel (GPU/DLA/EMC clock speed). It is typically 20-40 ms for most models. The DLA is more efficient than the GPU, but not faster, so using the DLA will reduce power consumption but will slightly increase inference time.
#### Rockchip SoC
Frigate supports SBCs with the following Rockchip SoCs:
- RK3566/RK3568
- RK3588/RK3588S
- RV1103/RV1106
- RK3562
Using the yolov8n model and an Orange Pi 5 Plus with RK3588 SoC inference speeds vary between 25-40 ms.
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.

View File

@@ -95,6 +95,7 @@ The following community supported builds are available:
`ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp5` - Frigate build optimized for nvidia Jetson devices running Jetpack 5
`ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp4` - Frigate build optimized for nvidia Jetson devices running Jetpack 4.6
`ghcr.io/blakeblackshear/frigate:stable-rk` - Frigate build for SBCs with Rockchip SoC
:::

View File

@@ -263,6 +263,10 @@ Returns the snapshot image from the latest event for the given camera and label
Returns the snapshot image from the specific point in that cameras recordings.
### `GET /api/<camera_name>/grid.jpg`
Returns the latest camera image with the regions grid overlaid.
### `GET /clips/<camera>-<id>.jpg`
JPG snapshot for the given camera and event id.
@@ -361,3 +365,7 @@ Recording retention config still applies to manual events, if frigate is configu
### `PUT /api/events/<event_id>/end`
End a specific manual event without a predetermined length.
### `POST /api/restart`
Restarts Frigate process.

View File

@@ -221,6 +221,10 @@ Topic to turn the PTZ autotracker for a camera on and off. Expected values are `
Topic with current state of the PTZ autotracker for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/ptz_autotracker/active`
Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/birdseye/set`
Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode

View File

@@ -19,7 +19,7 @@ Once logged in, you can generate an API key for Frigate in Settings.
### Set your API key
In Frigate, you can set the `PLUS_API_KEY` environment variable to enable the `SEND TO FRIGATE+` buttons on the events page. You can set it in your Docker Compose file or in your Docker run command. Home Assistant Addon users can set it under Settings > Addons > Frigate NVR > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch).
In Frigate, you can use an environment variable or a docker secret named `PLUS_API_KEY` to enable the `SEND TO FRIGATE+` buttons on the events page. Home Assistant Addon users can set it under Settings > Addons > Frigate NVR > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch).
:::caution

View File

@@ -191,7 +191,8 @@ class FrigateApp:
"i",
self.config.cameras[camera_name].onvif.autotracking.enabled,
),
"ptz_stopped": mp.Event(),
"ptz_tracking_active": mp.Event(),
"ptz_motor_stopped": mp.Event(),
"ptz_reset": mp.Event(),
"ptz_start_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
@@ -212,7 +213,7 @@ class FrigateApp:
# issue https://github.com/python/typeshed/issues/8799
# from mypy 0.981 onwards
}
self.ptz_metrics[camera_name]["ptz_stopped"].set()
self.ptz_metrics[camera_name]["ptz_motor_stopped"].set()
self.feature_metrics[camera_name] = {
"audio_enabled": mp.Value( # type: ignore[typeddict-item]
# issue https://github.com/python/typeshed/issues/8799
@@ -444,6 +445,7 @@ class FrigateApp:
self.config,
self.onvif_controller,
self.ptz_metrics,
self.dispatcher,
self.stop_event,
)
self.ptz_autotracker_thread.start()

View File

@@ -1,5 +1,6 @@
"""Websocket communicator."""
import errno
import json
import logging
import threading
@@ -12,7 +13,7 @@ from ws4py.server.wsgirefserver import (
WSGIServer,
)
from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.websocket import WebSocket
from ws4py.websocket import WebSocket as WebSocket_
from frigate.comms.dispatcher import Communicator
from frigate.config import FrigateConfig
@@ -20,6 +21,18 @@ from frigate.config import FrigateConfig
logger = logging.getLogger(__name__)
class WebSocket(WebSocket_):
def unhandled_error(self, error):
"""
Handles the unfriendly socket closures on the server side
without showing a confusing error message
"""
if hasattr(error, "errno") and error.errno == errno.ECONNRESET:
pass
else:
logging.getLogger("ws4py").exception("Failed to receive data")
class WebSocketClient(Communicator): # type: ignore[misc]
"""Frigate wrapper for ws client."""

View File

@@ -5,6 +5,7 @@ import json
import logging
import os
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt
@@ -47,6 +48,13 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
for secret_file in os.listdir("/run/secrets"):
if secret_file.startswith("FRIGATE_"):
FRIGATE_ENV_VARS[secret_file] = Path(
os.path.join("/run/secrets", secret_file)
).read_text()
DEFAULT_TRACKED_OBJECTS = ["person"]
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
@@ -171,7 +179,7 @@ class PtzAutotrackConfig(FrigateBaseModel):
timeout: int = Field(
default=10, title="Seconds to delay before returning to preset."
)
movement_weights: Optional[Union[float, List[float]]] = Field(
movement_weights: Optional[Union[str, List[str]]] = Field(
default=[],
title="Internal value used for PTZ movements based on the speed of your camera's motor.",
)
@@ -731,6 +739,9 @@ class CameraConfig(FrigateBaseModel):
default=60,
title="How long to wait for the image with the highest confidence score.",
)
webui_url: Optional[str] = Field(
title="URL to visit the camera directly from system page",
)
zones: Dict[str, ZoneConfig] = Field(
default_factory=dict, title="Zone configuration."
)

View File

@@ -60,7 +60,7 @@ REQUEST_REGION_GRID = "request_region_grid"
# Autotracking
AUTOTRACKING_MAX_AREA_RATIO = 0.5
AUTOTRACKING_MAX_AREA_RATIO = 0.6
AUTOTRACKING_MOTION_MIN_DISTANCE = 20
AUTOTRACKING_MOTION_MAX_POINTS = 500
AUTOTRACKING_MAX_MOVE_METRICS = 500

View File

@@ -0,0 +1,122 @@
import logging
from typing import Literal
import cv2
import cv2.dnn
import numpy as np
try:
from hide_warnings import hide_warnings
except: # noqa: E722
def hide_warnings(func):
pass
from pydantic import Field
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
logger = logging.getLogger(__name__)
DETECTOR_KEY = "rknn"
class RknnDetectorConfig(BaseDetectorConfig):
type: Literal[DETECTOR_KEY]
score_thresh: float = Field(
default=0.5, ge=0, le=1, title="Minimal confidence for detection."
)
nms_thresh: float = Field(
default=0.45, ge=0, le=1, title="IoU threshold for non-maximum suppression."
)
class Rknn(DetectionApi):
type_key = DETECTOR_KEY
def __init__(self, config: RknnDetectorConfig):
self.height = config.model.height
self.width = config.model.width
self.score_thresh = config.score_thresh
self.nms_thresh = config.nms_thresh
self.model_path = config.model.path or "/models/yolov8n-320x320.rknn"
from rknnlite.api import RKNNLite
self.rknn = RKNNLite(verbose=False)
if self.rknn.load_rknn(self.model_path) != 0:
logger.error("Error initializing rknn model.")
if self.rknn.init_runtime() != 0:
logger.error("Error initializing rknn runtime.")
def __del__(self):
self.rknn.release()
def postprocess(self, results):
"""
Processes yolov8 output.
Args:
results: array with shape: (1, 84, n, 1) where n depends on yolov8 model size (for 320x320 model n=2100)
Returns:
detections: array with shape (20, 6) with 20 rows of (class, confidence, y_min, x_min, y_max, x_max)
"""
results = np.transpose(results[0, :, :, 0]) # array shape (2100, 84)
classes = np.argmax(
results[:, 4:], axis=1
) # array shape (2100,); index of class with max confidence of each row
scores = np.max(
results[:, 4:], axis=1
) # array shape (2100,); max confidence of each row
# array shape (2100, 4); bounding box of each row
boxes = np.transpose(
np.vstack(
(
results[:, 0] - 0.5 * results[:, 2],
results[:, 1] - 0.5 * results[:, 3],
results[:, 2],
results[:, 3],
)
)
)
# indices of rows with confidence > SCORE_THRESH with Non-maximum Suppression (NMS)
result_boxes = cv2.dnn.NMSBoxes(
boxes, scores, self.score_thresh, self.nms_thresh, 0.5
)
detections = np.zeros((20, 6), np.float32)
for i in range(len(result_boxes)):
if i >= 20:
break
index = result_boxes[i]
detections[i] = [
classes[index],
scores[index],
(boxes[index][1]) / self.height,
(boxes[index][0]) / self.width,
(boxes[index][1] + boxes[index][3]) / self.height,
(boxes[index][0] + boxes[index][2]) / self.width,
]
return detections
@hide_warnings
def inference(self, tensor_input):
return self.rknn.inference(inputs=tensor_input)
def detect_raw(self, tensor_input):
output = self.inference(
[
tensor_input,
]
)
return self.postprocess(output[0])

View File

@@ -293,6 +293,16 @@ class TensorRtDetector(DetectionApi):
# raw_detections: Nx7 numpy arrays of
# [[x, y, w, h, box_confidence, class_id, class_prob],
# throw out any detections with negative class IDs
valid_detections = []
for r in raw_detections:
if r[5] >= 0:
valid_detections.append(r)
else:
logger.warning(f"Found TensorRT detection with invalid class id {r}")
raw_detections = valid_detections
# Calculate score as box_confidence x class_prob
raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6]
# Reorder elements by the score, best on top, remove class_prob
@@ -303,6 +313,7 @@ class TensorRtDetector(DetectionApi):
ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1)
# put result into the correct order and limit to top 20
detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20]
# pad to 20x6 shape
append_cnt = 20 - len(detections)
if append_cnt > 0:

View File

@@ -240,7 +240,10 @@ class AudioEventMaintainer(threading.Thread):
rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float))))
# Transform RMS to dBFS (decibels relative to full scale)
dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE)
if rms > 0:
dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE)
else:
dBFS = 0
self.inter_process_communicator.queue.put(
(f"{self.config.name}/audio/dBFS", float(dBFS))

View File

@@ -55,7 +55,6 @@ _user_agent_args = [
]
PRESETS_HW_ACCEL_DECODE = {
"preset-rpi-32-h264": "-c:v:1 h264_v4l2m2m",
"preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m",
"preset-vaapi": f"-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format vaapi",
"preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v h264_qsv",
@@ -68,7 +67,6 @@ PRESETS_HW_ACCEL_DECODE = {
}
PRESETS_HW_ACCEL_SCALE = {
"preset-rpi-32-h264": "-r {0} -vf fps={0},scale={1}:{2}",
"preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}",
"preset-vaapi": "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=yuv420p",
"preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p",
@@ -81,7 +79,6 @@ PRESETS_HW_ACCEL_SCALE = {
}
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
"preset-rpi-32-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
"preset-vaapi": "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {1}",
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
@@ -94,8 +91,7 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
}
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rpi-32-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}",
"preset-vaapi": "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi {1}",
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",

View File

@@ -41,7 +41,7 @@ from frigate.const import (
RECORD_DIR,
)
from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Recordings, Timeline
from frigate.models import Event, Recordings, Regions, Timeline
from frigate.object_processing import TrackedObject
from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController
@@ -726,6 +726,112 @@ def label_snapshot(camera_name, label):
return response
@bp.route("/<camera_name>/grid.jpg")
def grid_snapshot(camera_name):
request.args.get("type", default="region")
if camera_name in current_app.frigate_config.cameras:
detect = current_app.frigate_config.cameras[camera_name].detect
frame = current_app.detected_frames_processor.get_current_frame(camera_name, {})
retry_interval = float(
current_app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
or 10
)
if frame is None or datetime.now().timestamp() > (
current_app.detected_frames_processor.get_current_frame_time(camera_name)
+ retry_interval
):
return make_response(
jsonify({"success": False, "message": "Unable to get valid frame"}),
500,
)
try:
grid = (
Regions.select(Regions.grid)
.where(Regions.camera == camera_name)
.get()
.grid
)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Unable to get region grid"}),
500,
)
grid_size = len(grid)
grid_coef = 1.0 / grid_size
width = detect.width
height = detect.height
for x in range(grid_size):
for y in range(grid_size):
cell = grid[x][y]
if len(cell["sizes"]) == 0:
continue
std_dev = round(cell["std_dev"] * width, 2)
mean = round(cell["mean"] * width, 2)
cv2.rectangle(
frame,
(int(x * grid_coef * width), int(y * grid_coef * height)),
(
int((x + 1) * grid_coef * width),
int((y + 1) * grid_coef * height),
),
(0, 255, 0),
2,
)
cv2.putText(
frame,
f"#: {len(cell['sizes'])}",
(
int(x * grid_coef * width + 10),
int((y * grid_coef + 0.02) * height),
),
cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.5,
color=(0, 255, 0),
thickness=2,
)
cv2.putText(
frame,
f"std: {std_dev}",
(
int(x * grid_coef * width + 10),
int((y * grid_coef + 0.05) * height),
),
cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.5,
color=(0, 255, 0),
thickness=2,
)
cv2.putText(
frame,
f"avg: {mean}",
(
int(x * grid_coef * width + 10),
int((y * grid_coef + 0.08) * height),
),
cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.5,
color=(0, 255, 0),
thickness=2,
)
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response
else:
return make_response(
jsonify({"success": False, "message": "Camera not found"}),
404,
)
@bp.route("/events/<id>/clip.mp4")
def event_clip(id):
download = request.args.get("download", type=bool)
@@ -946,7 +1052,7 @@ def events():
if is_submitted is not None:
if is_submitted == 0:
clauses.append((Event.plus_id.is_null()))
else:
elif is_submitted > 0:
clauses.append((Event.plus_id != ""))
if len(clauses) == 0:
@@ -1388,6 +1494,8 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
.limit(1)
)
try:
@@ -1995,3 +2103,30 @@ def logs(service: str):
jsonify({"success": False, "message": "Could not find log file"}),
500,
)
@bp.route("/restart", methods=["POST"])
def restart():
try:
restart_frigate()
except Exception as e:
logging.error(f"Error restarting Frigate: {e}")
return make_response(
jsonify(
{
"success": False,
"message": "Unable to restart Frigate.",
}
),
500,
)
return make_response(
jsonify(
{
"success": True,
"message": "Restarting (this can take up to one minute)...",
}
),
200,
)

View File

@@ -248,10 +248,8 @@ class TrackedObject:
if self.obj_data["frame_time"] - self.previous["frame_time"] > 60:
significant_change = True
# update autotrack at half fps
if self.obj_data["frame_time"] - self.previous["frame_time"] > (
1 / (self.camera_config.detect.fps / 2)
):
# update autotrack at most 3 objects per second
if self.obj_data["frame_time"] - self.previous["frame_time"] >= (1 / 3):
autotracker_update = True
self.obj_data.update(obj_data)

View File

@@ -63,8 +63,8 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
a_w, a_h = get_standard_aspect_ratio(width, height)
if round(a_w / a_h, 2) != round(width / height, 2):
canvas_width = width
canvas_height = int((canvas_width / a_w) * a_h)
canvas_width = int(width // 4 * 4)
canvas_height = int((canvas_width / a_w * a_h) // 4 * 4)
logger.warning(
f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
)

View File

@@ -3,6 +3,7 @@ import json
import logging
import os
import re
from pathlib import Path
from typing import Any, List
import cv2
@@ -36,6 +37,10 @@ class PlusApi:
self.key = None
if PLUS_ENV_VAR in os.environ:
self.key = os.environ.get(PLUS_ENV_VAR)
elif os.path.isdir("/run/secrets") and PLUS_ENV_VAR in os.listdir(
"/run/secrets"
):
self.key = Path(os.path.join("/run/secrets", PLUS_ENV_VAR)).read_text()
# check for the addon options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:

View File

@@ -18,6 +18,7 @@ from norfair.camera_motion import (
TranslationTransformationGetter,
)
from frigate.comms.dispatcher import Dispatcher
from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum
from frigate.const import (
AUTOTRACKING_MAX_AREA_RATIO,
@@ -144,11 +145,12 @@ class PtzAutoTrackerThread(threading.Thread):
config: FrigateConfig,
onvif: OnvifController,
ptz_metrics: dict[str, PTZMetricsTypes],
dispatcher: Dispatcher,
stop_event: MpEvent,
) -> None:
threading.Thread.__init__(self)
self.name = "ptz_autotracker"
self.ptz_autotracker = PtzAutoTracker(config, onvif, ptz_metrics)
self.ptz_autotracker = PtzAutoTracker(config, onvif, ptz_metrics, dispatcher)
self.stop_event = stop_event
self.config = config
@@ -175,10 +177,12 @@ class PtzAutoTracker:
config: FrigateConfig,
onvif: OnvifController,
ptz_metrics: PTZMetricsTypes,
dispatcher: Dispatcher,
) -> None:
self.config = config
self.onvif = onvif
self.ptz_metrics = ptz_metrics
self.dispatcher = dispatcher
self.tracked_object: dict[str, object] = {}
self.tracked_object_history: dict[str, object] = {}
self.tracked_object_metrics: dict[str, object] = {}
@@ -215,8 +219,8 @@ class PtzAutoTracker:
maxlen=round(camera_config.detect.fps * 1.5)
)
self.tracked_object_metrics[camera] = {
"max_target_box": 1
- (AUTOTRACKING_MAX_AREA_RATIO ** self.zoom_factor[camera])
"max_target_box": AUTOTRACKING_MAX_AREA_RATIO
** (1 / self.zoom_factor[camera])
}
self.calibrating[camera] = False
@@ -268,6 +272,10 @@ class PtzAutoTracker:
if camera_config.onvif.autotracking.movement_weights:
if len(camera_config.onvif.autotracking.movement_weights) == 5:
camera_config.onvif.autotracking.movement_weights = [
float(val)
for val in camera_config.onvif.autotracking.movement_weights
]
self.ptz_metrics[camera][
"ptz_min_zoom"
].value = camera_config.onvif.autotracking.movement_weights[0]
@@ -290,6 +298,8 @@ class PtzAutoTracker:
if camera_config.onvif.autotracking.calibrate_on_startup:
self._calibrate_camera(camera)
self.ptz_metrics[camera]["ptz_tracking_active"].clear()
self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False)
self.autotracker_init[camera] = True
def _write_config(self, camera):
@@ -334,7 +344,7 @@ class PtzAutoTracker:
1,
)
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
zoom_out_values.append(self.ptz_metrics[camera]["ptz_zoom_level"].value)
@@ -345,7 +355,7 @@ class PtzAutoTracker:
1,
)
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
zoom_in_values.append(self.ptz_metrics[camera]["ptz_zoom_level"].value)
@@ -363,7 +373,7 @@ class PtzAutoTracker:
1,
)
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
zoom_out_values.append(
@@ -379,7 +389,7 @@ class PtzAutoTracker:
1,
)
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
zoom_in_values.append(
@@ -402,10 +412,10 @@ class PtzAutoTracker:
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
)
self.ptz_metrics[camera]["ptz_reset"].set()
self.ptz_metrics[camera]["ptz_stopped"].clear()
self.ptz_metrics[camera]["ptz_motor_stopped"].clear()
# Wait until the camera finishes moving
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
for step in range(num_steps):
@@ -416,7 +426,7 @@ class PtzAutoTracker:
self.onvif._move_relative(camera, pan, tilt, 0, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
stop_time = time.time()
@@ -434,10 +444,10 @@ class PtzAutoTracker:
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
)
self.ptz_metrics[camera]["ptz_reset"].set()
self.ptz_metrics[camera]["ptz_stopped"].clear()
self.ptz_metrics[camera]["ptz_motor_stopped"].clear()
# Wait until the camera finishes moving
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
logger.info(
@@ -521,7 +531,11 @@ class PtzAutoTracker:
camera_height = camera_config.frame_shape[0]
# Extract areas and calculate weighted average
areas = [obj["area"] for obj in self.tracked_object_history[camera]]
# grab the largest dimension of the bounding box and create a square from that
areas = [
max(obj["box"][2] - obj["box"][0], obj["box"][3] - obj["box"][1]) ** 2
for obj in self.tracked_object_history[camera]
]
filtered_areas = (
remove_outliers(areas)
@@ -598,7 +612,9 @@ class PtzAutoTracker:
self.onvif._move_relative(camera, pan, tilt, 0, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera][
"ptz_motor_stopped"
].is_set():
self.onvif.get_camera_status(camera)
if (
@@ -608,7 +624,7 @@ class PtzAutoTracker:
self.onvif._zoom_absolute(camera, zoom, 1)
# Wait until the camera finishes moving
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
if self.config.cameras[camera].onvif.autotracking.movement_weights:
@@ -686,19 +702,20 @@ class PtzAutoTracker:
camera_height = camera_config.frame_shape[0]
camera_fps = camera_config.detect.fps
# estimate_velocity is a numpy array of bbox top,left and bottom,right velocities
velocities = obj.obj_data["estimate_velocity"]
logger.debug(f"{camera}: Velocities from norfair: {velocities}")
# if we are close enough to zero, return right away
if np.all(np.round(velocities) == 0):
return True, np.zeros((2, 2))
return True, np.zeros((4,))
# Thresholds
x_mags_thresh = camera_width / camera_fps / 2
y_mags_thresh = camera_height / camera_fps / 2
dir_thresh = 0.93
delta_thresh = 12
var_thresh = 5
delta_thresh = 20
var_thresh = 10
# Check magnitude
x_mags = np.abs(velocities[:, 0])
@@ -722,7 +739,6 @@ class PtzAutoTracker:
np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1])
)
dir_thresh = 0.6 if np.all(delta < delta_thresh / 2) else dir_thresh
print(f"cosine sim: {cosine_sim}")
invalid_dirs = cosine_sim < dir_thresh
# Combine
@@ -752,10 +768,10 @@ class PtzAutoTracker:
)
)
# invalid velocity
return False, np.zeros((2, 2))
return False, np.zeros((4,))
else:
logger.debug(f"{camera}: Valid velocity ")
return True, np.mean(velocities, axis=0)
return True, velocities.flatten()
def _get_distance_threshold(self, camera, obj):
# Returns true if Euclidean distance from object to center of frame is
@@ -836,7 +852,7 @@ class PtzAutoTracker:
# ensure object is not moving quickly
below_velocity_threshold = np.all(
np.abs(average_velocity)
< np.array([velocity_threshold_x, velocity_threshold_y])
< np.tile([velocity_threshold_x, velocity_threshold_y], 2)
) or np.all(average_velocity == 0)
below_area_threshold = (
@@ -938,7 +954,7 @@ class PtzAutoTracker:
camera_height = camera_config.frame_shape[0]
camera_fps = camera_config.detect.fps
average_velocity = np.zeros((2, 2))
average_velocity = np.zeros((4,))
predicted_box = obj.obj_data["box"]
centroid_x = obj.obj_data["centroid"][0]
@@ -966,7 +982,6 @@ class PtzAutoTracker:
# this box could exceed the frame boundaries if velocity is high
# but we'll handle that in _enqueue_move() as two separate moves
current_box = np.array(obj.obj_data["box"])
average_velocity = np.tile(average_velocity, 2)
predicted_box = (
current_box
+ camera_fps * predicted_movement_time * average_velocity
@@ -1010,7 +1025,10 @@ class PtzAutoTracker:
zoom = 0
result = None
current_zoom_level = self.ptz_metrics[camera]["ptz_zoom_level"].value
target_box = obj.obj_data["area"] / (camera_width * camera_height)
target_box = max(
obj.obj_data["box"][2] - obj.obj_data["box"][0],
obj.obj_data["box"][3] - obj.obj_data["box"][1],
) ** 2 / (camera_width * camera_height)
# absolute zooming separately from pan/tilt
if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.absolute:
@@ -1061,24 +1079,15 @@ class PtzAutoTracker:
< self.tracked_object_metrics[camera]["max_target_box"]
else self.tracked_object_metrics[camera]["max_target_box"]
)
zoom = (
2
* (
limit
/ (
self.tracked_object_metrics[camera]["target_box"]
+ limit
)
)
- 1
)
ratio = limit / self.tracked_object_metrics[camera]["target_box"]
zoom = (ratio - 1) / (ratio + 1)
logger.debug(f"{camera}: Zoom calculation: {zoom}")
if not result:
# zoom out with special condition if zooming out because of velocity, edges, etc.
zoom = -(1 - zoom) if zoom > 0 else -(zoom + 1)
zoom = -(1 - zoom) if zoom > 0 else -(zoom * 2 + 1)
if result:
# zoom in
zoom = 1 - zoom if zoom > 0 else (zoom + 1)
zoom = 1 - zoom if zoom > 0 else (zoom * 2 + 1)
logger.debug(f"{camera}: Zooming: {result} Zoom amount: {zoom}")
@@ -1117,6 +1126,10 @@ class PtzAutoTracker:
logger.debug(
f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}"
)
self.ptz_metrics[camera]["ptz_tracking_active"].set()
self.dispatcher.publish(
f"{camera}/ptz_autotracker/active", "ON", retain=False
)
self.tracked_object[camera] = obj
self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data))
@@ -1199,8 +1212,8 @@ class PtzAutoTracker:
)
self.tracked_object[camera] = None
self.tracked_object_metrics[camera] = {
"max_target_box": 1
- (AUTOTRACKING_MAX_AREA_RATIO ** self.zoom_factor[camera])
"max_target_box": AUTOTRACKING_MAX_AREA_RATIO
** (1 / self.zoom_factor[camera])
}
def camera_maintenance(self, camera):
@@ -1219,7 +1232,7 @@ class PtzAutoTracker:
if not self.autotracker_init[camera]:
self._autotracker_setup(self.config.cameras[camera], camera)
# regularly update camera status
if not self.ptz_metrics[camera]["ptz_stopped"].is_set():
if not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
# return to preset if tracking is over
@@ -1242,7 +1255,7 @@ class PtzAutoTracker:
while not self.move_queues[camera].empty():
self.move_queues[camera].get()
self.ptz_metrics[camera]["ptz_stopped"].wait()
self.ptz_metrics[camera]["ptz_motor_stopped"].wait()
logger.debug(
f"{camera}: Time is {self.ptz_metrics[camera]['ptz_frame_time'].value}, returning to preset: {autotracker_config.return_preset}"
)
@@ -1252,7 +1265,11 @@ class PtzAutoTracker:
)
# update stored zoom level from preset
if not self.ptz_metrics[camera]["ptz_stopped"].is_set():
if not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
self.onvif.get_camera_status(camera)
self.ptz_metrics[camera]["ptz_tracking_active"].clear()
self.dispatcher.publish(
f"{camera}/ptz_autotracker/active", "OFF", retain=False
)
self.ptz_metrics[camera]["ptz_reset"].set()

View File

@@ -299,7 +299,7 @@ class OnvifController:
return
self.cams[camera_name]["active"] = True
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
logger.debug(
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
)
@@ -366,7 +366,7 @@ class OnvifController:
return
self.cams[camera_name]["active"] = True
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
self.ptz_metrics[camera_name]["ptz_start_time"].value = 0
self.ptz_metrics[camera_name]["ptz_stop_time"].value = 0
move_request = self.cams[camera_name]["move_request"]
@@ -413,7 +413,7 @@ class OnvifController:
return
self.cams[camera_name]["active"] = True
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
logger.debug(
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
)
@@ -543,8 +543,8 @@ class OnvifController:
zoom_status is None or zoom_status.lower() == "idle"
):
self.cams[camera_name]["active"] = False
if not self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].set()
if not self.ptz_metrics[camera_name]["ptz_motor_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_motor_stopped"].set()
logger.debug(
f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
@@ -555,8 +555,8 @@ class OnvifController:
]["ptz_frame_time"].value
else:
self.cams[camera_name]["active"] = True
if self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
if self.ptz_metrics[camera_name]["ptz_motor_stopped"].is_set():
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
logger.debug(
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
@@ -586,7 +586,7 @@ class OnvifController:
# some hikvision cams won't update MoveStatus, so warn if it hasn't changed
if (
not self.ptz_metrics[camera_name]["ptz_stopped"].is_set()
not self.ptz_metrics[camera_name]["ptz_motor_stopped"].is_set()
and not self.ptz_metrics[camera_name]["ptz_reset"].is_set()
and self.ptz_metrics[camera_name]["ptz_start_time"].value != 0
and self.ptz_metrics[camera_name]["ptz_frame_time"].value

View File

@@ -3,17 +3,15 @@
import datetime
import itertools
import logging
import os
import threading
from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path
from peewee import DatabaseError, chunked
from frigate.config import FrigateConfig, RetainModeEnum
from frigate.const import CACHE_DIR, RECORD_DIR
from frigate.models import Event, Recordings, RecordingsToDelete
from frigate.record.util import remove_empty_directories
from frigate.models import Event, Recordings
from frigate.record.util import remove_empty_directories, sync_recordings
from frigate.util.builtin import get_tomorrow_at_time
logger = logging.getLogger(__name__)
@@ -180,76 +178,25 @@ class RecordingCleanup(threading.Thread):
logger.debug("End all cameras.")
logger.debug("End expire recordings.")
def sync_recordings(self) -> None:
"""Check the db for stale recordings entries that don't exist in the filesystem."""
logger.debug("Start sync recordings.")
# get all recordings in the db
recordings = Recordings.select(Recordings.id, Recordings.path)
# get all recordings files on disk and put them in a set
files_on_disk = {
os.path.join(root, file)
for root, _, files in os.walk(RECORD_DIR)
for file in files
}
# Use pagination to process records in chunks
page_size = 1000
num_pages = (recordings.count() + page_size - 1) // page_size
recordings_to_delete = set()
for page in range(num_pages):
for recording in recordings.paginate(page, page_size):
if recording.path not in files_on_disk:
recordings_to_delete.add(recording.id)
# convert back to list of dictionaries for insertion
recordings_to_delete = [
{"id": recording_id} for recording_id in recordings_to_delete
]
if len(recordings_to_delete) / max(1, recordings.count()) > 0.5:
logger.debug(
f"Deleting {(len(recordings_to_delete) / recordings.count()):2f}% of recordings could be due to configuration error. Aborting..."
)
return
logger.debug(
f"Deleting {len(recordings_to_delete)} recordings with missing files"
)
# create a temporary table for deletion
RecordingsToDelete.create_table(temporary=True)
# insert ids to the temporary table
max_inserts = 1000
for batch in chunked(recordings_to_delete, max_inserts):
RecordingsToDelete.insert_many(batch).execute()
try:
# delete records in the main table that exist in the temporary table
query = Recordings.delete().where(
Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id))
)
query.execute()
except DatabaseError as e:
logger.error(f"Database error during delete: {e}")
logger.debug("End sync recordings.")
def run(self) -> None:
# on startup sync recordings with disk if enabled
if self.config.record.sync_on_startup:
self.sync_recordings()
sync_recordings(limited=False)
next_sync = get_tomorrow_at_time(3)
# Expire tmp clips every minute, recordings and clean directories every hour.
for counter in itertools.cycle(range(self.config.record.expire_interval)):
if self.stop_event.wait(60):
logger.info("Exiting recording cleanup...")
break
self.clean_tmp_clips()
if datetime.datetime.now().astimezone(datetime.timezone.utc) > next_sync:
sync_recordings(limited=True)
next_sync = get_tomorrow_at_time(3)
if counter == 0:
self.expire_recordings()
remove_empty_directories(RECORD_DIR)

View File

@@ -1,7 +1,16 @@
"""Recordings Utilities."""
import datetime
import logging
import os
from peewee import DatabaseError, chunked
from frigate.const import RECORD_DIR
from frigate.models import Recordings, RecordingsToDelete
logger = logging.getLogger(__name__)
def remove_empty_directories(directory: str) -> None:
# list all directories recursively and sort them by path,
@@ -17,3 +26,110 @@ def remove_empty_directories(directory: str) -> None:
continue
if len(os.listdir(path)) == 0:
os.rmdir(path)
def sync_recordings(limited: bool) -> None:
"""Check the db for stale recordings entries that don't exist in the filesystem."""
def delete_db_entries_without_file(files_on_disk: list[str]) -> bool:
"""Delete db entries where file was deleted outside of frigate."""
if limited:
recordings = Recordings.select(Recordings.id, Recordings.path).where(
Recordings.start_time
>= (datetime.datetime.now() - datetime.timedelta(hours=36)).timestamp()
)
else:
# get all recordings in the db
recordings = Recordings.select(Recordings.id, Recordings.path)
# Use pagination to process records in chunks
page_size = 1000
num_pages = (recordings.count() + page_size - 1) // page_size
recordings_to_delete = set()
for page in range(num_pages):
for recording in recordings.paginate(page, page_size):
if recording.path not in files_on_disk:
recordings_to_delete.add(recording.id)
# convert back to list of dictionaries for insertion
recordings_to_delete = [
{"id": recording_id} for recording_id in recordings_to_delete
]
if float(len(recordings_to_delete)) / max(1, recordings.count()) > 0.5:
logger.debug(
f"Deleting {(float(len(recordings_to_delete)) / recordings.count()):2f}% of recordings DB entries, could be due to configuration error. Aborting..."
)
return False
logger.debug(
f"Deleting {len(recordings_to_delete)} recording DB entries with missing files"
)
# create a temporary table for deletion
RecordingsToDelete.create_table(temporary=True)
# insert ids to the temporary table
max_inserts = 1000
for batch in chunked(recordings_to_delete, max_inserts):
RecordingsToDelete.insert_many(batch).execute()
try:
# delete records in the main table that exist in the temporary table
query = Recordings.delete().where(
Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id))
)
query.execute()
except DatabaseError as e:
logger.error(f"Database error during recordings db cleanup: {e}")
return True
def delete_files_without_db_entry(files_on_disk: list[str]):
"""Delete files where file is not inside frigate db."""
files_to_delete = []
for file in files_on_disk:
if not Recordings.select().where(Recordings.path == file).exists():
files_to_delete.append(file)
if float(len(files_to_delete)) / max(1, len(files_on_disk)) > 0.5:
logger.debug(
f"Deleting {(float(len(files_to_delete)) / len(files_on_disk)):2f}% of recordings DB entries, could be due to configuration error. Aborting..."
)
return
for file in files_to_delete:
os.unlink(file)
logger.debug("Start sync recordings.")
if limited:
# get recording files from last 36 hours
hour_check = (
datetime.datetime.now().astimezone(datetime.timezone.utc)
- datetime.timedelta(hours=36)
).strftime("%Y-%m-%d/%H")
files_on_disk = {
os.path.join(root, file)
for root, _, files in os.walk(RECORD_DIR)
for file in files
if file > hour_check
}
else:
# get all recordings files on disk and put them in a set
files_on_disk = {
os.path.join(root, file)
for root, _, files in os.walk(RECORD_DIR)
for file in files
}
db_success = delete_db_entries_without_file(files_on_disk)
# only try to cleanup files if db cleanup was successful
if db_success:
delete_files_without_db_entry(files_on_disk)
logger.debug("End sync recordings.")

View File

@@ -159,9 +159,13 @@ class StorageMaintainer(threading.Thread):
# Delete recordings not retained indefinitely
if not keep:
deleted_segments_size += recording.segment_size
Path(recording.path).unlink(missing_ok=True)
deleted_recordings.add(recording.id)
try:
Path(recording.path).unlink(missing_ok=False)
deleted_recordings.add(recording.id)
deleted_segments_size += recording.segment_size
except FileNotFoundError:
# this file was not found so we must assume no space was cleaned up
pass
# check if need to delete retained segments
if deleted_segments_size < hourly_bandwidth:
@@ -183,9 +187,15 @@ class StorageMaintainer(threading.Thread):
if deleted_segments_size > hourly_bandwidth:
break
deleted_segments_size += recording.segment_size
Path(recording.path).unlink(missing_ok=True)
deleted_recordings.add(recording.id)
try:
Path(recording.path).unlink(missing_ok=False)
deleted_segments_size += recording.segment_size
deleted_recordings.add(recording.id)
except FileNotFoundError:
# this file was not found so we must assume no space was cleaned up
pass
else:
logger.info(f"Cleaned up {deleted_segments_size} MB of recordings")
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
# delete up to 100,000 at a time

View File

@@ -1651,11 +1651,11 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config()
assert runtime_config.cameras["back"].onvif.autotracking.movement_weights == [
0,
1,
1.23,
2.34,
0.50,
"0.0",
"1.0",
"1.23",
"2.34",
"0.5",
]
def test_fails_invalid_movement_weights(self):

View File

@@ -1,6 +1,7 @@
import datetime
import logging
import os
import tempfile
import unittest
from unittest.mock import MagicMock
@@ -26,6 +27,7 @@ class TestHttp(unittest.TestCase):
self.db = SqliteQueueDatabase(TEST_DB)
models = [Event, Recordings]
self.db.bind(models)
self.test_dir = tempfile.mkdtemp()
self.minimal_config = {
"mqtt": {"host": "mqtt"},
@@ -94,6 +96,7 @@ class TestHttp(unittest.TestCase):
rec_bd_id = "1234568.backdoor"
_insert_mock_recording(
rec_fd_id,
os.path.join(self.test_dir, f"{rec_fd_id}.tmp"),
time_keep,
time_keep + 10,
camera="front_door",
@@ -102,6 +105,7 @@ class TestHttp(unittest.TestCase):
)
_insert_mock_recording(
rec_bd_id,
os.path.join(self.test_dir, f"{rec_bd_id}.tmp"),
time_keep + 10,
time_keep + 20,
camera="back_door",
@@ -123,6 +127,7 @@ class TestHttp(unittest.TestCase):
rec_fd_id = "1234567.frontdoor"
_insert_mock_recording(
rec_fd_id,
os.path.join(self.test_dir, f"{rec_fd_id}.tmp"),
time_keep,
time_keep + 10,
camera="front_door",
@@ -141,13 +146,33 @@ class TestHttp(unittest.TestCase):
id = "123456.keep"
time_keep = datetime.datetime.now().timestamp()
_insert_mock_event(id, time_keep, time_keep + 30, True)
_insert_mock_event(
id,
time_keep,
time_keep + 30,
True,
)
rec_k_id = "1234567.keep"
rec_k2_id = "1234568.keep"
rec_k3_id = "1234569.keep"
_insert_mock_recording(rec_k_id, time_keep, time_keep + 10)
_insert_mock_recording(rec_k2_id, time_keep + 10, time_keep + 20)
_insert_mock_recording(rec_k3_id, time_keep + 20, time_keep + 30)
_insert_mock_recording(
rec_k_id,
os.path.join(self.test_dir, f"{rec_k_id}.tmp"),
time_keep,
time_keep + 10,
)
_insert_mock_recording(
rec_k2_id,
os.path.join(self.test_dir, f"{rec_k2_id}.tmp"),
time_keep + 10,
time_keep + 20,
)
_insert_mock_recording(
rec_k3_id,
os.path.join(self.test_dir, f"{rec_k3_id}.tmp"),
time_keep + 20,
time_keep + 30,
)
id2 = "7890.delete"
time_delete = datetime.datetime.now().timestamp() - 360
@@ -155,9 +180,24 @@ class TestHttp(unittest.TestCase):
rec_d_id = "78901.delete"
rec_d2_id = "78902.delete"
rec_d3_id = "78903.delete"
_insert_mock_recording(rec_d_id, time_delete, time_delete + 10)
_insert_mock_recording(rec_d2_id, time_delete + 10, time_delete + 20)
_insert_mock_recording(rec_d3_id, time_delete + 20, time_delete + 30)
_insert_mock_recording(
rec_d_id,
os.path.join(self.test_dir, f"{rec_d_id}.tmp"),
time_delete,
time_delete + 10,
)
_insert_mock_recording(
rec_d2_id,
os.path.join(self.test_dir, f"{rec_d2_id}.tmp"),
time_delete + 10,
time_delete + 20,
)
_insert_mock_recording(
rec_d3_id,
os.path.join(self.test_dir, f"{rec_d3_id}.tmp"),
time_delete + 20,
time_delete + 30,
)
storage.calculate_camera_bandwidth()
storage.reduce_storage_consumption()
@@ -176,18 +216,42 @@ class TestHttp(unittest.TestCase):
id = "123456.keep"
time_keep = datetime.datetime.now().timestamp()
_insert_mock_event(id, time_keep, time_keep + 30, True)
_insert_mock_event(
id,
time_keep,
time_keep + 30,
True,
)
rec_k_id = "1234567.keep"
rec_k2_id = "1234568.keep"
rec_k3_id = "1234569.keep"
_insert_mock_recording(rec_k_id, time_keep, time_keep + 10)
_insert_mock_recording(rec_k2_id, time_keep + 10, time_keep + 20)
_insert_mock_recording(rec_k3_id, time_keep + 20, time_keep + 30)
_insert_mock_recording(
rec_k_id,
os.path.join(self.test_dir, f"{rec_k_id}.tmp"),
time_keep,
time_keep + 10,
)
_insert_mock_recording(
rec_k2_id,
os.path.join(self.test_dir, f"{rec_k2_id}.tmp"),
time_keep + 10,
time_keep + 20,
)
_insert_mock_recording(
rec_k3_id,
os.path.join(self.test_dir, f"{rec_k3_id}.tmp"),
time_keep + 20,
time_keep + 30,
)
time_delete = datetime.datetime.now().timestamp() - 7200
for i in range(0, 59):
id = f"{123456 + i}.delete"
_insert_mock_recording(
f"{123456 + i}.delete", time_delete, time_delete + 600
id,
os.path.join(self.test_dir, f"{id}.tmp"),
time_delete,
time_delete + 600,
)
storage.calculate_camera_bandwidth()
@@ -219,13 +283,23 @@ def _insert_mock_event(id: str, start: int, end: int, retain: bool) -> Event:
def _insert_mock_recording(
id: str, start: int, end: int, camera="front_door", seg_size=8, seg_dur=10
id: str,
file: str,
start: int,
end: int,
camera="front_door",
seg_size=8,
seg_dur=10,
) -> Event:
"""Inserts a basic recording model with a given id."""
# we must open the file so storage maintainer will delete it
with open(file, "w"):
pass
return Recordings.insert(
id=id,
camera=camera,
path=f"/recordings/{id}",
path=file,
start_time=start,
end_time=end,
duration=seg_dur,

View File

@@ -31,7 +31,8 @@ class CameraMetricsTypes(TypedDict):
class PTZMetricsTypes(TypedDict):
ptz_autotracker_enabled: Synchronized
ptz_stopped: Event
ptz_tracking_active: Event
ptz_motor_stopped: Event
ptz_reset: Event
ptz_start_time: Synchronized
ptz_stop_time: Synchronized

View File

@@ -114,10 +114,8 @@ def load_config_with_no_duplicates(raw_config) -> dict:
def clean_camera_user_pass(line: str) -> str:
"""Removes user and password from line."""
if "rtsp://" in line:
return re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line)
else:
return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", line)
rtsp_cleaned = re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line)
return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", rtsp_cleaned)
def escape_special_characters(path: str) -> str:
@@ -265,8 +263,9 @@ def find_by_key(dictionary, target_key):
return None
def get_tomorrow_at_2() -> datetime.datetime:
def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am."""
tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1)
return tomorrow.replace(hour=2, minute=0, second=0).astimezone(
return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(
datetime.timezone.utc
)

View File

@@ -174,9 +174,9 @@ def get_region_from_grid(
cell = region_grid[grid_x][grid_y]
# if there is no known data, get standard region for motion box
# if there is no known data, use original region calculation
if not cell or not cell["sizes"]:
return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region)
return box
# convert the calculated region size to relative
calc_size = (box[2] - box[0]) / frame_shape[1]

View File

@@ -26,7 +26,7 @@ from frigate.ptz.autotrack import ptz_moving_at_frame_time
from frigate.track import ObjectTracker
from frigate.track.norfair_tracker import NorfairTracker
from frigate.types import PTZMetricsTypes
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_2
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_time
from frigate.util.image import (
FrameManager,
SharedMemoryFrameManager,
@@ -528,7 +528,7 @@ def process_frames(
fps = process_info["process_fps"]
detection_fps = process_info["detection_fps"]
current_frame_time = process_info["detection_frame"]
next_region_update = get_tomorrow_at_2()
next_region_update = get_tomorrow_at_time(2)
fps_tracker = EventsPerSecond()
fps_tracker.start()
@@ -550,7 +550,7 @@ def process_frames(
except queue.Empty:
logger.error(f"Unable to get updated region grid for {camera_name}")
next_region_update = get_tomorrow_at_2()
next_region_update = get_tomorrow_at_time(2)
try:
if exit_on_empty:

301
web/package-lock.json generated
View File

@@ -1664,16 +1664,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
"integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz",
"integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/type-utils": "6.8.0",
"@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/type-utils": "6.9.1",
"@typescript-eslint/utils": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -1870,15 +1870,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
"integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
"integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/typescript-estree": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4"
},
"engines": {
@@ -1898,13 +1898,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
"integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz",
"integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0"
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1915,13 +1915,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
"integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz",
"integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/typescript-estree": "6.9.1",
"@typescript-eslint/utils": "6.9.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -1942,9 +1942,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
"integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz",
"integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1955,13 +1955,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
"integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz",
"integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1997,17 +1997,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz",
"integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/typescript-estree": "6.9.1",
"semver": "^7.5.4"
},
"engines": {
@@ -2037,12 +2037,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
"integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz",
"integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/types": "6.9.1",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -2060,17 +2060,17 @@
"dev": true
},
"node_modules/@videojs/http-streaming": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
"integrity": "sha512-dty8lsZk9QPc0i4It79tjWsmPiaC3FpgARFM0vJGko4k3yKNZIYkAk8kjiDRfkAQH/HZ3rYi5dDTriFNzwSsIg==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.7.0.tgz",
"integrity": "sha512-5uLFKBL8CvD56dxxJyuxqB5CY0tdoa4SE9KbXakeiAy6iFBUEPvTr2YGLKEWvQ8Lojs1wl+FQndLdv+GO7t9Fw==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "4.0.0",
"aes-decrypter": "4.0.1",
"global": "^4.4.0",
"m3u8-parser": "^7.1.0",
"mpd-parser": "^1.1.1",
"mux.js": "7.0.0",
"mpd-parser": "^1.2.2",
"mux.js": "7.0.1",
"video.js": "^7 || ^8"
},
"engines": {
@@ -2105,22 +2105,6 @@
"npm": ">=5"
}
},
"node_modules/@videojs/http-streaming/node_modules/mux.js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.0.tgz",
"integrity": "sha512-DeZmr+3NDrO02k4SREtl4VB5GyGPCz2fzMjDxBIlamkxffSTLge97rtNMoonnmFHTp96QggDucUtKv3fmyObrA==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
},
"bin": {
"muxjs-transmux": "bin/transmux.js"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/@videojs/vhs-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz",
@@ -2669,9 +2653,9 @@
}
},
"node_modules/axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -4078,9 +4062,9 @@
}
},
"node_modules/eslint-plugin-jest": {
"version": "27.4.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
"integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
"version": "27.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz",
"integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==",
"dev": true,
"dependencies": {
"@typescript-eslint/utils": "^5.10.0"
@@ -6126,9 +6110,9 @@
}
},
"node_modules/jiti": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
@@ -6929,9 +6913,9 @@
"dev": true
},
"node_modules/mux.js": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz",
"integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.1.tgz",
"integrity": "sha512-Omz79uHqYpMP1V80JlvEdCiOW1hiw4mBvDh9gaZEpxvB+7WYb2soZSzfuSRrK2Kh9Pm6eugQNrIpY/Bnyhk4hw==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
@@ -8666,9 +8650,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -8676,10 +8660,10 @@
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -9169,12 +9153,12 @@
}
},
"node_modules/video.js": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.5.2.tgz",
"integrity": "sha512-6/uNXQV3xSaKLpaPf/bVvr7omd+82sKUp0RMBgIt4PxHIe28GtX+O+GcNfI2fuwBvcDRDqk5Ei5AG9bJJOpulA==",
"version": "8.6.1",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.6.1.tgz",
"integrity": "sha512-CNYVJ5WWIZ7bOhbkkfcKqLGoc6WsE3Ft2RfS1lXdQTWk8UiSsPW2Ssk2JzPCA8qnIlUG9os/faCFsYWjyu4JcA==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "3.5.3",
"@videojs/http-streaming": "3.7.0",
"@videojs/vhs-utils": "^4.0.0",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "^4.0.1",
@@ -9182,7 +9166,7 @@
"keycode": "2.2.0",
"m3u8-parser": "^6.0.0",
"mpd-parser": "^1.0.1",
"mux.js": "^6.2.0",
"mux.js": "^7.0.1",
"safe-json-parse": "4.0.0",
"videojs-contrib-quality-levels": "4.0.0",
"videojs-font": "4.1.0",
@@ -10837,16 +10821,16 @@
}
},
"@typescript-eslint/eslint-plugin": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
"integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz",
"integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/type-utils": "6.8.0",
"@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/type-utils": "6.9.1",
"@typescript-eslint/utils": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -10960,54 +10944,54 @@
}
},
"@typescript-eslint/parser": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
"integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
"integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/typescript-estree": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
"integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz",
"integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0"
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1"
}
},
"@typescript-eslint/type-utils": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
"integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz",
"integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/utils": "6.8.0",
"@typescript-eslint/typescript-estree": "6.9.1",
"@typescript-eslint/utils": "6.9.1",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
}
},
"@typescript-eslint/types": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
"integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz",
"integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
"integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz",
"integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/visitor-keys": "6.8.0",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/visitor-keys": "6.9.1",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -11027,17 +11011,17 @@
}
},
"@typescript-eslint/utils": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
"integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz",
"integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.8.0",
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/typescript-estree": "6.8.0",
"@typescript-eslint/scope-manager": "6.9.1",
"@typescript-eslint/types": "6.9.1",
"@typescript-eslint/typescript-estree": "6.9.1",
"semver": "^7.5.4"
},
"dependencies": {
@@ -11053,12 +11037,12 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
"integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz",
"integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "6.8.0",
"@typescript-eslint/types": "6.9.1",
"eslint-visitor-keys": "^3.4.1"
}
},
@@ -11069,17 +11053,17 @@
"dev": true
},
"@videojs/http-streaming": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
"integrity": "sha512-dty8lsZk9QPc0i4It79tjWsmPiaC3FpgARFM0vJGko4k3yKNZIYkAk8kjiDRfkAQH/HZ3rYi5dDTriFNzwSsIg==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.7.0.tgz",
"integrity": "sha512-5uLFKBL8CvD56dxxJyuxqB5CY0tdoa4SE9KbXakeiAy6iFBUEPvTr2YGLKEWvQ8Lojs1wl+FQndLdv+GO7t9Fw==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "4.0.0",
"aes-decrypter": "4.0.1",
"global": "^4.4.0",
"m3u8-parser": "^7.1.0",
"mpd-parser": "^1.1.1",
"mux.js": "7.0.0",
"mpd-parser": "^1.2.2",
"mux.js": "7.0.1",
"video.js": "^7 || ^8"
},
"dependencies": {
@@ -11104,15 +11088,6 @@
}
}
}
},
"mux.js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.0.tgz",
"integrity": "sha512-DeZmr+3NDrO02k4SREtl4VB5GyGPCz2fzMjDxBIlamkxffSTLge97rtNMoonnmFHTp96QggDucUtKv3fmyObrA==",
"requires": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
}
}
}
},
@@ -11520,9 +11495,9 @@
"dev": true
},
"axios": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -12598,9 +12573,9 @@
}
},
"eslint-plugin-jest": {
"version": "27.4.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
"integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
"version": "27.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz",
"integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==",
"dev": true,
"requires": {
"@typescript-eslint/utils": "^5.10.0"
@@ -13962,9 +13937,9 @@
}
},
"jiti": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
"dev": true
},
"js-levenshtein": {
@@ -14565,9 +14540,9 @@
"dev": true
},
"mux.js": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz",
"integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.1.tgz",
"integrity": "sha512-Omz79uHqYpMP1V80JlvEdCiOW1hiw4mBvDh9gaZEpxvB+7WYb2soZSzfuSRrK2Kh9Pm6eugQNrIpY/Bnyhk4hw==",
"requires": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
@@ -15814,9 +15789,9 @@
}
},
"tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dev": true,
"requires": {
"@alloc/quick-lru": "^5.2.0",
@@ -15824,10 +15799,10 @@
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -16203,12 +16178,12 @@
}
},
"video.js": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.5.2.tgz",
"integrity": "sha512-6/uNXQV3xSaKLpaPf/bVvr7omd+82sKUp0RMBgIt4PxHIe28GtX+O+GcNfI2fuwBvcDRDqk5Ei5AG9bJJOpulA==",
"version": "8.6.1",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.6.1.tgz",
"integrity": "sha512-CNYVJ5WWIZ7bOhbkkfcKqLGoc6WsE3Ft2RfS1lXdQTWk8UiSsPW2Ssk2JzPCA8qnIlUG9os/faCFsYWjyu4JcA==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "3.5.3",
"@videojs/http-streaming": "3.7.0",
"@videojs/vhs-utils": "^4.0.0",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "^4.0.1",
@@ -16216,7 +16191,7 @@
"keycode": "2.2.0",
"m3u8-parser": "^6.0.0",
"mpd-parser": "^1.0.1",
"mux.js": "^6.2.0",
"mux.js": "^7.0.1",
"safe-json-parse": "4.0.0",
"videojs-contrib-quality-levels": "4.0.0",
"videojs-font": "4.1.0",

View File

@@ -67,6 +67,7 @@ export default function Button({
disabled = false,
ariaCapitalize = false,
href,
target,
type = 'contained',
...attrs
}) {
@@ -101,6 +102,7 @@ export default function Button({
tabindex="0"
className={classes}
href={href}
target={target}
ref={ref}
onmouseenter={handleMousenter}
onmouseleave={handleMouseleave}

View File

@@ -0,0 +1,19 @@
import { h } from 'preact';
import { memo } from 'preact/compat';
export function Submitted({ className = 'h-6 w-6', inner_fill = 'none', outer_stroke = 'currentColor', onClick = () => {} }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
viewBox="0 0 32 32"
onClick={onClick}
>
<rect x="10" y="15" fill={inner_fill} width="12" height="2"/>
<rect x="15" y="10" fill={inner_fill} width="2" height="12"/>
<circle fill="none" stroke={outer_stroke} stroke-width="2" stroke-miterlimit="10" cx="16" cy="16" r="12"/>
</svg>
);
}
export default memo(Submitted);

21
web/src/icons/WebUI.jsx Normal file
View File

@@ -0,0 +1,21 @@
import { h } from 'preact';
import { memo } from 'preact/compat';
export function WebUI({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
fill={fill}
viewBox="0 0 24 24"
stroke={stroke}
onClick={onClick}
>
<path
d="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z"
/>
</svg>
);
}
export default memo(WebUI);

View File

@@ -11,6 +11,7 @@ import axios from 'axios';
import { useState, useRef, useCallback, useMemo } from 'preact/hooks';
import VideoPlayer from '../components/VideoPlayer';
import { StarRecording } from '../icons/StarRecording';
import { Submitted } from '../icons/Submitted';
import { Snapshot } from '../icons/Snapshot';
import { UploadPlus } from '../icons/UploadPlus';
import { Clip } from '../icons/Clip';
@@ -63,6 +64,7 @@ export default function Events({ path, ...props }) {
time_range: '00:00,24:00',
timezone,
favorites: props.favorites ?? 0,
is_submitted: props.is_submitted ?? -1,
event: props.event,
});
const [state, setState] = useState({
@@ -281,6 +283,16 @@ export default function Events({ path, ...props }) {
[path, searchParams, setSearchParams]
);
const onClickFilterSubmitted = useCallback(
() => {
if( ++searchParams.is_submitted > 1 ) {
searchParams.is_submitted = -1;
}
onFilter('is_submitted', searchParams.is_submitted);
},
[searchParams, onFilter]
);
const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT;
// hooks for infinite scroll
@@ -394,11 +406,22 @@ export default function Events({ path, ...props }) {
</Button>
)}
<StarRecording
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
onClick={() => onFilter('favorites', searchParams.favorites ? 0 : 1)}
fill={searchParams.favorites == 1 ? 'currentColor' : 'none'}
/>
<div className="ml-auto flex">
{config.plus.enabled && (
<Submitted
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
onClick={() => onClickFilterSubmitted()}
inner_fill={searchParams.is_submitted == 1 ? 'currentColor' : 'gray'}
outer_stroke={searchParams.is_submitted >= 0 ? 'currentColor' : 'gray'}
/>
)}
<StarRecording
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
onClick={() => onFilter('favorites', searchParams.favorites ? 0 : 1)}
fill={searchParams.favorites == 1 ? 'currentColor' : 'none'}
/>
</div>
<div ref={datePicker} className="ml-right">
<CalendarIcon

View File

@@ -12,6 +12,7 @@ import Dialog from '../components/Dialog';
import TimeAgo from '../components/TimeAgo';
import copy from 'copy-to-clipboard';
import { About } from '../icons/About';
import { WebUI } from '../icons/WebUI';
const emptyObject = Object.freeze({});
@@ -347,7 +348,17 @@ export default function System() {
>
<div className="capitalize text-lg flex justify-between p-4">
<Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
<Button onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
<div className="flex">
{config.cameras[camera]['webui_url'] && (
<Button
href={config.cameras[camera]['webui_url']}
target="_blank"
>
Web UI<WebUI className="ml-1 h-4 w-4" fill="white" stroke="white" />
</Button>
)}
<Button className="ml-2" onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
</div>
</div>
<div className="p-2">
<Table className="w-full">