forked from Github/frigate
Compare commits
24 Commits
pass_filte
...
v0.13.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65e3e67a83 | ||
|
|
63233a5830 | ||
|
|
4f7b710112 | ||
|
|
ac53993f70 | ||
|
|
ef750e73a2 | ||
|
|
7270eef6bf | ||
|
|
b54aaad382 | ||
|
|
fc36be4f88 | ||
|
|
aefecad4c0 | ||
|
|
c57528cbcf | ||
|
|
090294e89b | ||
|
|
a6279a0337 | ||
|
|
0dd3dd23aa | ||
|
|
4bd29b2ee8 | ||
|
|
cc79cbcadc | ||
|
|
89366d7b12 | ||
|
|
6eff08eb2d | ||
|
|
8b6b83bd62 | ||
|
|
8b6e3a0d37 | ||
|
|
8a9b26df4e | ||
|
|
fd6a3bd5d2 | ||
|
|
8085ad4b4c | ||
|
|
af24eb7dbf | ||
|
|
d1620b4e39 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
/docker/tensorrt/ @madsciencetist @NateMeyer
|
||||
/docker/tensorrt/*arm64* @madsciencetist
|
||||
/docker/tensorrt/*jetson* @madsciencetist
|
||||
|
||||
/docker/rockchip/ @MarcA711
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
24
docker/rockchip/Dockerfile
Normal file
24
docker/rockchip/Dockerfile
Normal 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/
|
||||
2
docker/rockchip/requirements-wheels-rk.txt
Normal file
2
docker/rockchip/requirements-wheels-rk.txt
Normal 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
34
docker/rockchip/rk.hcl
Normal 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
10
docker/rockchip/rk.mk
Normal 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
|
||||
BIN
docker/rockchip/yolov8n-320x320.rknn
Normal file
BIN
docker/rockchip/yolov8n-320x320.rknn
Normal file
Binary file not shown.
@@ -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.
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
122
frigate/detectors/plugins/rknn.py
Normal file
122
frigate/detectors/plugins/rknn.py
Normal 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])
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}",
|
||||
|
||||
139
frigate/http.py
139
frigate/http.py
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
301
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
19
web/src/icons/Submitted.jsx
Normal file
19
web/src/icons/Submitted.jsx
Normal 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
21
web/src/icons/WebUI.jsx
Normal 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);
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user