Compare commits

..

1 Commits

Author SHA1 Message Date
Blake Blackshear
1d58e419f4 add release workflow for images 2023-10-28 06:34:15 -05:00
56 changed files with 406 additions and 1118 deletions

View File

@@ -79,15 +79,6 @@ jobs:
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64 *.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max *.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: jetson_jp4_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Jetson Jetpack 4 name: Jetson Jetpack 4
@@ -150,7 +141,7 @@ jobs:
- arm64_build - arm64_build
steps: steps:
- id: lowercaseRepo - id: lowercaseRepo
uses: ASzc/change-string-case-action@v6 uses: ASzc/change-string-case-action@v5
with: with:
string: ${{ github.repository }} string: ${{ github.repository }}
- name: Log in to the Container registry - name: Log in to the Container registry

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
set -euxo pipefail set -euxo pipefail
NGINX_VERSION="1.25.3" NGINX_VERSION="1.25.2"
VOD_MODULE_VERSION="1.31" VOD_MODULE_VERSION="1.31"
SECURE_TOKEN_MODULE_VERSION="1.5" SECURE_TOKEN_MODULE_VERSION="1.5"
RTMP_MODULE_VERSION="1.2.2" RTMP_MODULE_VERSION="1.2.2"

View File

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

View File

@@ -3,7 +3,6 @@
import json import json
import os import os
import sys import sys
from pathlib import Path
import yaml import yaml
@@ -17,14 +16,6 @@ sys.path.remove("/opt/frigate")
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("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") config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml # Check if we can use .yaml instead of .yml

View File

@@ -32,8 +32,6 @@ http {
gzip_proxied no-cache no-store private expired auth; gzip_proxied no-cache no-store private expired auth;
gzip_vary on; gzip_vary on;
proxy_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=api_cache:10m max_size=10m inactive=1m use_temp_path=off;
upstream frigate_api { upstream frigate_api {
server 127.0.0.1:5001; server 127.0.0.1:5001;
keepalive 1024; keepalive 1024;
@@ -187,19 +185,6 @@ http {
proxy_pass http://frigate_api/; proxy_pass http://frigate_api/;
include proxy.conf; include proxy.conf;
proxy_cache api_cache;
proxy_cache_lock on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 5s;
proxy_cache_bypass $http_x_cache_bypass;
add_header X-Cache-Status $upstream_cache_status;
location /api/vod/ {
proxy_pass http://frigate_api/vod/;
include proxy.conf;
proxy_cache off;
}
location /api/stats { location /api/stats {
access_log off; access_log off;
rewrite ^/api/(.*)$ $1 break; rewrite ^/api/(.*)$ $1 break;

View File

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

View File

@@ -1,2 +0,0 @@
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

View File

@@ -1,34 +0,0 @@
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"]
}

View File

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

Binary file not shown.

View File

@@ -31,7 +31,7 @@ First, set up a PTZ preset in your camera's firmware and give it a name. If you'
Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset. 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. 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. An [ONVIF connection](cameras.md) is required for autotracking to function.
Note that `autotracking` is disabled by default but can be enabled in the configuration or by MQTT. 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. 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 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. 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.
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. A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's debug viewer for your camera to see a thicker colored box around the object currently being autotracked.

View File

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

View File

@@ -75,11 +75,11 @@ mqtt:
# NOTE: must be unique if you are running multiple instances # NOTE: must be unique if you are running multiple instances
client_id: frigate client_id: frigate
# Optional: user # Optional: user
# NOTE: MQTT user can be specified with an environment variables or docker secrets that must begin with 'FRIGATE_'. # NOTE: MQTT user can be specified with an environment variables that must begin with 'FRIGATE_'.
# e.g. user: '{FRIGATE_MQTT_USER}' # e.g. user: '{FRIGATE_MQTT_USER}'
user: mqtt_user user: mqtt_user
# Optional: password # Optional: password
# NOTE: MQTT password can be specified with an environment variables or docker secrets that must begin with 'FRIGATE_'. # NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
# e.g. password: '{FRIGATE_MQTT_PASSWORD}' # e.g. password: '{FRIGATE_MQTT_PASSWORD}'
password: password password: password
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None) # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
@@ -231,8 +231,6 @@ detect:
fps: 5 fps: 5
# Optional: enables detection for the camera (default: True) # Optional: enables detection for the camera (default: True)
enabled: True enabled: True
# Optional: Number of consecutive detection hits required for an object to be initialized in the tracker. (default: 1/2 the frame rate)
min_initialized: 2
# Optional: Number of frames without a detection before Frigate considers an object to be gone. (default: 5x the frame rate) # Optional: Number of frames without a detection before Frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25 max_disappeared: 25
# Optional: Configuration for stationary object tracking # Optional: Configuration for stationary object tracking
@@ -491,7 +489,7 @@ cameras:
# Required: A list of input streams for the camera. See documentation for more information. # Required: A list of input streams for the camera. See documentation for more information.
inputs: inputs:
# Required: the path to the stream # Required: the path to the stream
# NOTE: path may include environment variables or docker secrets, which must begin with 'FRIGATE_' and be referenced in {} # NOTE: path may include environment variables, 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 - 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 # 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, # NOTICE: In addition to assigning the audio, record, and rtmp roles,
@@ -520,9 +518,6 @@ cameras:
# to be replaced by a newer image. (default: shown below) # to be replaced by a newer image. (default: shown below)
best_image_timeout: 60 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 # Optional: zones for this camera
zones: zones:
# Required: name of the zone # Required: name of the zone

View File

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

View File

@@ -95,16 +95,6 @@ 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. 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) ## 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. This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.

View File

@@ -95,7 +95,6 @@ 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-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-tensorrt-jp4` - Frigate build optimized for nvidia Jetson devices running Jetpack 4.6
`ghcr.io/blakeblackshear/frigate:stable-rk` - Frigate build for SBCs with Rockchip SoC
::: :::

View File

@@ -263,10 +263,6 @@ 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. 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` ### `GET /clips/<camera>-<id>.jpg`
JPG snapshot for the given camera and event id. JPG snapshot for the given camera and event id.
@@ -365,7 +361,3 @@ Recording retention config still applies to manual events, if frigate is configu
### `PUT /api/events/<event_id>/end` ### `PUT /api/events/<event_id>/end`
End a specific manual event without a predetermined length. End a specific manual event without a predetermined length.
### `POST /api/restart`
Restarts Frigate process.

View File

@@ -221,10 +221,6 @@ 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`. 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` ### `frigate/<camera_name>/birdseye/set`
Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode

View File

@@ -19,7 +19,7 @@ Once logged in, you can generate an API key for Frigate in Settings.
### Set your API key ### Set your API key
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). 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).
:::caution :::caution

View File

@@ -9,34 +9,6 @@ With a subscription, and at each annual renewal, you will receive 12 model train
Information on how to integrate Frigate+ with Frigate can be found in the [integrations docs](/integrations/plus). Information on how to integrate Frigate+ with Frigate can be found in the [integrations docs](/integrations/plus).
## Improving your model
You may find that Frigate+ models result in more false positives initially, but by submitting true and false positives, the model will improve. Because a limited number of users submitted images to Frigate+ prior to this launch, you may need to submit several hundred images per camera to see good results. With all the new images now being submitted, future base models will improve as more and more users (including you) submit examples to Frigate+.
False positives can be reduced by submitting **both** true positives and false positives. This will help the model differentiate between what is and isn't correct. You should aim for a target of 80% true positive submissions and 20% false positives across all of your images. If you are experiencing false positives in a specific area, submitting true positives for any object type near that area in similar lighting conditions will help teach the model what that area looks like when no objects are present.
You may find that it's helpful to lower your thresholds a little in order to generate more false/true positives near the threshold value. For example, if you have some false positives that are scoring at 68% and some true positives scoring at 72%, you can try lowering your threshold to 65% and submitting both true and false positives within that range. This will help the model learn and widen the gap between true and false positive scores.
Note that only verified images will be used when training your model. Submitting an image from Frigate as a true or false positive will not verify the image. You still must verify the image in Frigate+ in order for it to be used in training.
In order to request your first model, you will need to have annotated and verified at least 10 images. Each subsequent model request will require that 10 additional images are verified. However, this is the bare minimum. For the best results, you should provide at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night.
As circumstances change, you may need to submit new examples to address new types of false positives. For example, the change from summer days to snowy winter days or other changes such as a new grill or patio furniture may require additional examples and training.
## Properly labeling images
For the best results, follow the following guidelines.
**Label every object in the image**: It is important that you label all objects in each image before verifying. If you don't label a car for example, the model will be taught that part of the image is _not_ a car and it will start to get confused.
**Make tight bounding boxes**: Tighter bounding boxes improve the recognition and ensure that accurate bounding boxes are predicted at runtime.
**Label the full object even when occluded**: If you have a person standing behind a car, label the full person even though a portion of their body may be hidden behind the car. This helps predict accurate bounding boxes and improves zone accuracy and filters at runtime.
**`amazon`, `ups`, and `fedex` should label the logo**: For a Fedex truck, label the truck as a `car` and make a different bounding box just for the Fedex logo. If there are multiple logos, label each of them.
![Fedex Logo](/img/plus/fedex-logo.jpg)
## Frequently asked questions ## Frequently asked questions
### Are my models trained just on my image uploads? How are they built? ### Are my models trained just on my image uploads? How are they built?
@@ -45,7 +17,7 @@ Frigate+ models are built by fine tuning a base model with the images you have a
### What is a training credit and how do I use them? ### What is a training credit and how do I use them?
Essentially, `1 training credit = 1 trained model`. When you have uploaded, annotated, and verified additional images and you are ready to train your model, you will submit a model request which will use one credit. The model that is trained will utilize all of the verified images in your account. When new base models are available, it will require the use of a training credit to generate a new user model on the new base model. Essentially, `1 training credit = 1 trained model`. When you have uploaded, annotated, and verified additional images and you are ready to train your model, you will submit a model request which will use one credit. The model that is trained will utilize all of the verified images in your account.
### Are my video feeds sent to the cloud for analysis when using Frigate+ models? ### Are my video feeds sent to the cloud for analysis when using Frigate+ models?
@@ -137,3 +109,31 @@ When using Frigate+ models, Frigate will choose the snapshot of a person object
`amazon`, `ups`, and `fedex` labels are used to automatically assign a sub label to car objects. `amazon`, `ups`, and `fedex` labels are used to automatically assign a sub label to car objects.
![Fedex Attribute](/img/plus/attribute-example-fedex.jpg) ![Fedex Attribute](/img/plus/attribute-example-fedex.jpg)
## Properly labeling images
For the best results, follow the following guidelines.
**Label every object in the image**: It is important that you label all objects in each image before verifying. If you don't label a car for example, the model will be taught that part of the image is _not_ a car and it will start to get confused.
**Make tight bounding boxes**: Tighter bounding boxes improve the recognition and ensure that accurate bounding boxes are predicted at runtime.
**Label the full object even when occluded**: If you have a person standing behind a car, label the full person even though a portion of their body may be hidden behind the car. This helps predict accurate bounding boxes and improves zone accuracy and filters at runtime.
**`amazon`, `ups`, and `fedex` should label the logo**: For a Fedex truck, label the truck as a `car` and make a different bounding box just for the Fedex logo. If there are multiple logos, label each of them.
![Fedex Logo](/img/plus/fedex-logo.jpg)
## Improving your model
You may find that Frigate+ models result in more false positives initially, but by submitting true and false positives, the model will improve. This may be because your cameras don't look quite enough like the user submissions that were used to train the base model. Over time, this will improve as more and more users (including you) submit examples to Frigate+.
False positives can be reduced by submitting **both** true positives and false positives. This will help the model differentiate between what is and isn't correct.
You may find that it's helpful to lower your thresholds a little in order to generate more false/true positives near the threshold value. For example, if you have some false positives that are scoring at 68% and some true positives scoring at 72%, you can try lowering your threshold to 65% and submitting both true and false positives within that range. This will help the model learn and widen the gap between true and false positive scores.
Note that only verified images will be used when training your model. Submitting an image from Frigate as a true or false positive will not verify the image. You still must verify the image in Frigate+ in order for it to be used in training.
In order to request your first model, you will need to have annotated and verified at least 10 images. Each subsequent model request will require that 10 additional images are verified. However, this is the bare minimum. For the best results, you should provide at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night.
As circumstances change, you may need to submit new examples to address new types of false positives. For example, the change from summer days to snowy winter days or other changes such as a new grill or patio furniture may require additional examples and training.

View File

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

View File

@@ -1,6 +1,5 @@
"""Websocket communicator.""" """Websocket communicator."""
import errno
import json import json
import logging import logging
import threading import threading
@@ -13,7 +12,7 @@ from ws4py.server.wsgirefserver import (
WSGIServer, WSGIServer,
) )
from ws4py.server.wsgiutils import WebSocketWSGIApplication from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.websocket import WebSocket as WebSocket_ from ws4py.websocket import WebSocket
from frigate.comms.dispatcher import Communicator from frigate.comms.dispatcher import Communicator
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@@ -21,18 +20,6 @@ from frigate.config import FrigateConfig
logger = logging.getLogger(__name__) 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] class WebSocketClient(Communicator): # type: ignore[misc]
"""Frigate wrapper for ws client.""" """Frigate wrapper for ws client."""

View File

@@ -5,7 +5,6 @@ import json
import logging import logging
import os import os
from enum import Enum from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -48,13 +47,6 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
# DEFAULT_TIME_FORMAT = "%d.%m.%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_")} 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_TRACKED_OBJECTS = ["person"]
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"] DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
@@ -179,7 +171,7 @@ class PtzAutotrackConfig(FrigateBaseModel):
timeout: int = Field( timeout: int = Field(
default=10, title="Seconds to delay before returning to preset." default=10, title="Seconds to delay before returning to preset."
) )
movement_weights: Optional[Union[str, List[str]]] = Field( movement_weights: Optional[Union[float, List[float]]] = Field(
default=[], default=[],
title="Internal value used for PTZ movements based on the speed of your camera's motor.", title="Internal value used for PTZ movements based on the speed of your camera's motor.",
) )
@@ -360,9 +352,6 @@ class DetectConfig(FrigateBaseModel):
default=5, title="Number of frames per second to process through detection." default=5, title="Number of frames per second to process through detection."
) )
enabled: bool = Field(default=True, title="Detection Enabled.") enabled: bool = Field(default=True, title="Detection Enabled.")
min_initialized: Optional[int] = Field(
title="Minimum number of consecutive hits for an object to be initialized by the tracker."
)
max_disappeared: Optional[int] = Field( max_disappeared: Optional[int] = Field(
title="Maximum number of frames the object can dissapear before detection ends." title="Maximum number of frames the object can dissapear before detection ends."
) )
@@ -739,9 +728,6 @@ class CameraConfig(FrigateBaseModel):
default=60, default=60,
title="How long to wait for the image with the highest confidence score.", 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( zones: Dict[str, ZoneConfig] = Field(
default_factory=dict, title="Zone configuration." default_factory=dict, title="Zone configuration."
) )
@@ -1157,11 +1143,6 @@ class FrigateConfig(FrigateBaseModel):
else DEFAULT_DETECT_DIMENSIONS["height"] else DEFAULT_DETECT_DIMENSIONS["height"]
) )
# Default min_initialized configuration
min_initialized = camera_config.detect.fps / 2
if camera_config.detect.min_initialized is None:
camera_config.detect.min_initialized = min_initialized
# Default max_disappeared configuration # Default max_disappeared configuration
max_disappeared = camera_config.detect.fps * 5 max_disappeared = camera_config.detect.fps * 5
if camera_config.detect.max_disappeared is None: if camera_config.detect.max_disappeared is None:

View File

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

View File

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

View File

@@ -293,16 +293,6 @@ class TensorRtDetector(DetectionApi):
# raw_detections: Nx7 numpy arrays of # raw_detections: Nx7 numpy arrays of
# [[x, y, w, h, box_confidence, class_id, class_prob], # [[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 # Calculate score as box_confidence x class_prob
raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6] raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6]
# Reorder elements by the score, best on top, remove class_prob # Reorder elements by the score, best on top, remove class_prob
@@ -313,7 +303,6 @@ class TensorRtDetector(DetectionApi):
ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1) ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1)
# put result into the correct order and limit to top 20 # put result into the correct order and limit to top 20
detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20] detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20]
# pad to 20x6 shape # pad to 20x6 shape
append_cnt = 20 - len(detections) append_cnt = 20 - len(detections)
if append_cnt > 0: if append_cnt > 0:

View File

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

View File

@@ -106,10 +106,10 @@ class ExternalEventProcessor:
# write jpg snapshot with optional annotations # write jpg snapshot with optional annotations
if draw.get("boxes") and isinstance(draw.get("boxes"), list): if draw.get("boxes") and isinstance(draw.get("boxes"), list):
for box in draw.get("boxes"): for box in draw.get("boxes"):
x = int(box["box"][0] * camera_config.detect.width) x = box["box"][0] * camera_config.detect.width
y = int(box["box"][1] * camera_config.detect.height) y = box["box"][1] * camera_config.detect.height
width = int(box["box"][2] * camera_config.detect.width) width = box["box"][2] * camera_config.detect.width
height = int(box["box"][3] * camera_config.detect.height) height = box["box"][3] * camera_config.detect.height
draw_box_with_label( draw_box_with_label(
img_frame, img_frame,

View File

@@ -55,6 +55,7 @@ _user_agent_args = [
] ]
PRESETS_HW_ACCEL_DECODE = { PRESETS_HW_ACCEL_DECODE = {
"preset-rpi-32-h264": "-c:v:1 h264_v4l2m2m",
"preset-rpi-64-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-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", "preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v h264_qsv",
@@ -67,6 +68,7 @@ PRESETS_HW_ACCEL_DECODE = {
} }
PRESETS_HW_ACCEL_SCALE = { 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-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-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", "preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p",
@@ -79,6 +81,7 @@ PRESETS_HW_ACCEL_SCALE = {
} }
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { 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-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-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}", "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}",
@@ -91,7 +94,8 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
} }
PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = { PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -pix_fmt yuv420p {1}", "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 {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-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}", "preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",

View File

@@ -41,7 +41,7 @@ from frigate.const import (
RECORD_DIR, RECORD_DIR,
) )
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Recordings, Regions, Timeline from frigate.models import Event, Recordings, Timeline
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
@@ -726,112 +726,6 @@ def label_snapshot(camera_name, label):
return response 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") @bp.route("/events/<id>/clip.mp4")
def event_clip(id): def event_clip(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
@@ -1052,7 +946,7 @@ def events():
if is_submitted is not None: if is_submitted is not None:
if is_submitted == 0: if is_submitted == 0:
clauses.append((Event.plus_id.is_null())) clauses.append((Event.plus_id.is_null()))
elif is_submitted > 0: else:
clauses.append((Event.plus_id != "")) clauses.append((Event.plus_id != ""))
if len(clauses) == 0: if len(clauses) == 0:
@@ -1494,8 +1388,6 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
) )
) )
.where(Recordings.camera == camera_name) .where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.desc())
.limit(1)
) )
try: try:
@@ -2103,30 +1995,3 @@ def logs(service: str):
jsonify({"success": False, "message": "Could not find log file"}), jsonify({"success": False, "message": "Could not find log file"}),
500, 500,
) )
@bp.route("/restart", methods=["POST"])
def restart():
try:
restart_frigate()
except Exception as e:
logging.error(f"Error restarting Frigate: {e}")
return make_response(
jsonify(
{
"success": False,
"message": "Unable to restart Frigate.",
}
),
500,
)
return make_response(
jsonify(
{
"success": True,
"message": "Restarting (this can take up to one minute)...",
}
),
200,
)

View File

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

View File

@@ -63,8 +63,8 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
a_w, a_h = get_standard_aspect_ratio(width, height) a_w, a_h = get_standard_aspect_ratio(width, height)
if round(a_w / a_h, 2) != round(width / height, 2): if round(a_w / a_h, 2) != round(width / height, 2):
canvas_width = int(width // 4 * 4) canvas_width = width
canvas_height = int((canvas_width / a_w * a_h) // 4 * 4) canvas_height = int((canvas_width / a_w) * a_h)
logger.warning( logger.warning(
f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}" f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
) )
@@ -463,7 +463,7 @@ class BirdsEyeFrameManager:
def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]: def calculate_layout(self, cameras_to_add: list[str], coefficient) -> tuple[any]:
"""Calculate the optimal layout for 2+ cameras.""" """Calculate the optimal layout for 2+ cameras."""
def map_layout(camera_layout: list[list[any]], row_height: int): def map_layout(row_height: int):
"""Map the calculated layout.""" """Map the calculated layout."""
candidate_layout = [] candidate_layout = []
starting_x = 0 starting_x = 0
@@ -492,7 +492,7 @@ class BirdsEyeFrameManager:
x + scaled_width > self.canvas.width x + scaled_width > self.canvas.width
or y + scaled_height > self.canvas.height or y + scaled_height > self.canvas.height
): ):
return x + scaled_width, y + scaled_height, None return 0, 0, None
final_row.append((cameras[0], (x, y, scaled_width, scaled_height))) final_row.append((cameras[0], (x, y, scaled_width, scaled_height)))
x += scaled_width x += scaled_width
@@ -564,24 +564,10 @@ class BirdsEyeFrameManager:
return None return None
row_height = int(self.canvas.height / coefficient) row_height = int(self.canvas.height / coefficient)
total_width, total_height, standard_candidate_layout = map_layout( total_width, total_height, standard_candidate_layout = map_layout(row_height)
camera_layout, row_height
)
if not standard_candidate_layout: if not standard_candidate_layout:
# if standard layout didn't work return None
# try reducing row_height by the % overflow
scale_down_percent = max(
total_width / self.canvas.width,
total_height / self.canvas.height,
)
row_height = int(row_height / scale_down_percent)
total_width, total_height, standard_candidate_layout = map_layout(
camera_layout, row_height
)
if not standard_candidate_layout:
return None
# layout can't be optimized more # layout can't be optimized more
if total_width / self.canvas.width >= 0.99: if total_width / self.canvas.width >= 0.99:
@@ -592,7 +578,7 @@ class BirdsEyeFrameManager:
1 / (total_height / self.canvas.height), 1 / (total_height / self.canvas.height),
) )
row_height = int(row_height * scale_up_percent) row_height = int(row_height * scale_up_percent)
_, _, scaled_layout = map_layout(camera_layout, row_height) _, _, scaled_layout = map_layout(row_height)
if scaled_layout: if scaled_layout:
return scaled_layout return scaled_layout

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,17 @@
import datetime import datetime
import itertools import itertools
import logging import logging
import os
import threading import threading
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from pathlib import Path from pathlib import Path
from peewee import DatabaseError, chunked
from frigate.config import FrigateConfig, RetainModeEnum from frigate.config import FrigateConfig, RetainModeEnum
from frigate.const import CACHE_DIR, RECORD_DIR from frigate.const import CACHE_DIR, RECORD_DIR
from frigate.models import Event, Recordings from frigate.models import Event, Recordings, RecordingsToDelete
from frigate.record.util import remove_empty_directories, sync_recordings from frigate.record.util import remove_empty_directories
from frigate.util.builtin import get_tomorrow_at_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -178,25 +180,76 @@ class RecordingCleanup(threading.Thread):
logger.debug("End all cameras.") logger.debug("End all cameras.")
logger.debug("End expire recordings.") 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: def run(self) -> None:
# on startup sync recordings with disk if enabled # on startup sync recordings with disk if enabled
if self.config.record.sync_on_startup: if self.config.record.sync_on_startup:
sync_recordings(limited=False) self.sync_recordings()
next_sync = get_tomorrow_at_time(3)
# Expire tmp clips every minute, recordings and clean directories every hour. # Expire tmp clips every minute, recordings and clean directories every hour.
for counter in itertools.cycle(range(self.config.record.expire_interval)): for counter in itertools.cycle(range(self.config.record.expire_interval)):
if self.stop_event.wait(60): if self.stop_event.wait(60):
logger.info("Exiting recording cleanup...") logger.info("Exiting recording cleanup...")
break break
self.clean_tmp_clips() 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: if counter == 0:
self.expire_recordings() self.expire_recordings()
remove_empty_directories(RECORD_DIR) remove_empty_directories(RECORD_DIR)

View File

@@ -6,7 +6,6 @@ import os
import subprocess as sp import subprocess as sp
import threading import threading
from enum import Enum from enum import Enum
from pathlib import Path
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import EXPORT_DIR, MAX_PLAYLIST_SECONDS from frigate.const import EXPORT_DIR, MAX_PLAYLIST_SECONDS
@@ -122,7 +121,6 @@ class RecordingExporter(threading.Thread):
f"Failed to export recording for command {' '.join(ffmpeg_cmd)}" f"Failed to export recording for command {' '.join(ffmpeg_cmd)}"
) )
logger.error(p.stderr) logger.error(p.stderr)
Path(file_name).unlink(missing_ok=True)
return return
logger.debug(f"Updating finalized export {file_name}") logger.debug(f"Updating finalized export {file_name}")

View File

@@ -260,10 +260,8 @@ class RecordingMaintainer(threading.Thread):
most_recently_processed_frame_time = ( most_recently_processed_frame_time = (
camera_info[-1][0] if len(camera_info) > 0 else 0 camera_info[-1][0] if len(camera_info) > 0 else 0
) )
retain_cutoff = datetime.datetime.fromtimestamp( retain_cutoff = most_recently_processed_frame_time - pre_capture
most_recently_processed_frame_time - pre_capture if end_time.timestamp() < retain_cutoff:
).astimezone(datetime.timezone.utc)
if end_time.astimezone(datetime.timezone.utc) < retain_cutoff:
Path(cache_path).unlink(missing_ok=True) Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None) self.end_time_cache.pop(cache_path, None)
# else retain days includes this segment # else retain days includes this segment
@@ -275,11 +273,7 @@ class RecordingMaintainer(threading.Thread):
) )
# ensure delayed segment info does not lead to lost segments # ensure delayed segment info does not lead to lost segments
if datetime.datetime.fromtimestamp( if most_recently_processed_frame_time >= end_time.timestamp():
most_recently_processed_frame_time
).astimezone(datetime.timezone.utc) >= end_time.astimezone(
datetime.timezone.utc
):
record_mode = self.config.cameras[camera].record.retain.mode record_mode = self.config.cameras[camera].record.retain.mode
return await self.move_segment( return await self.move_segment(
camera, start_time, end_time, duration, cache_path, record_mode camera, start_time, end_time, duration, cache_path, record_mode

View File

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

View File

@@ -159,13 +159,9 @@ class StorageMaintainer(threading.Thread):
# Delete recordings not retained indefinitely # Delete recordings not retained indefinitely
if not keep: if not keep:
try: deleted_segments_size += recording.segment_size
Path(recording.path).unlink(missing_ok=False) Path(recording.path).unlink(missing_ok=True)
deleted_recordings.add(recording.id) 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 # check if need to delete retained segments
if deleted_segments_size < hourly_bandwidth: if deleted_segments_size < hourly_bandwidth:
@@ -187,15 +183,9 @@ class StorageMaintainer(threading.Thread):
if deleted_segments_size > hourly_bandwidth: if deleted_segments_size > hourly_bandwidth:
break break
try: deleted_segments_size += recording.segment_size
Path(recording.path).unlink(missing_ok=False) Path(recording.path).unlink(missing_ok=True)
deleted_segments_size += recording.segment_size deleted_recordings.add(recording.id)
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") logger.debug(f"Expiring {len(deleted_recordings)} recordings")
# delete up to 100,000 at a time # delete up to 100,000 at a time

View File

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

View File

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

View File

@@ -68,6 +68,7 @@ class NorfairTracker(ObjectTracker):
self.untracked_object_boxes: list[list[int]] = [] self.untracked_object_boxes: list[list[int]] = []
self.disappeared = {} self.disappeared = {}
self.positions = {} self.positions = {}
self.max_disappeared = config.detect.max_disappeared
self.camera_config = config self.camera_config = config
self.detect_config = config.detect self.detect_config = config.detect
self.ptz_metrics = ptz_metrics self.ptz_metrics = ptz_metrics
@@ -80,8 +81,8 @@ class NorfairTracker(ObjectTracker):
self.tracker = Tracker( self.tracker = Tracker(
distance_function=frigate_distance, distance_function=frigate_distance,
distance_threshold=2.5, distance_threshold=2.5,
initialization_delay=self.detect_config.min_initialized, initialization_delay=self.detect_config.fps / 2,
hit_counter_max=self.detect_config.max_disappeared, hit_counter_max=self.max_disappeared,
) )
if self.ptz_autotracker_enabled.value: if self.ptz_autotracker_enabled.value:
self.ptz_motion_estimator = PtzMotionEstimator( self.ptz_motion_estimator = PtzMotionEstimator(

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ from frigate.ptz.autotrack import ptz_moving_at_frame_time
from frigate.track import ObjectTracker from frigate.track import ObjectTracker
from frigate.track.norfair_tracker import NorfairTracker from frigate.track.norfair_tracker import NorfairTracker
from frigate.types import PTZMetricsTypes from frigate.types import PTZMetricsTypes
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_time from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_2
from frigate.util.image import ( from frigate.util.image import (
FrameManager, FrameManager,
SharedMemoryFrameManager, SharedMemoryFrameManager,
@@ -233,15 +233,14 @@ class CameraWatchdog(threading.Thread):
poll = p["process"].poll() poll = p["process"].poll()
if self.config.record.enabled and "record" in p["roles"]: if self.config.record.enabled and "record" in p["roles"]:
latest_segment_time = self.get_latest_segment_datetime( latest_segment_time = self.get_latest_segment_timestamp(
p.get( p.get(
"latest_segment_time", "latest_segment_time", datetime.datetime.now().timestamp()
datetime.datetime.now().astimezone(datetime.timezone.utc),
) )
) )
if datetime.datetime.now().astimezone(datetime.timezone.utc) > ( if datetime.datetime.now().timestamp() > (
latest_segment_time + datetime.timedelta(seconds=120) latest_segment_time + 120
): ):
self.logger.error( self.logger.error(
f"No new recording segments were created for {self.camera_name} in the last 120s. restarting the ffmpeg record process..." f"No new recording segments were created for {self.camera_name} in the last 120s. restarting the ffmpeg record process..."
@@ -289,7 +288,7 @@ class CameraWatchdog(threading.Thread):
) )
self.capture_thread.start() self.capture_thread.start()
def get_latest_segment_datetime(self, latest_segment: datetime.datetime) -> int: def get_latest_segment_timestamp(self, latest_timestamp) -> int:
"""Checks if ffmpeg is still writing recording segments to cache.""" """Checks if ffmpeg is still writing recording segments to cache."""
cache_files = sorted( cache_files = sorted(
[ [
@@ -300,15 +299,13 @@ class CameraWatchdog(threading.Thread):
and not d.startswith("clip_") and not d.startswith("clip_")
] ]
) )
newest_segment_timestamp = latest_segment newest_segment_timestamp = latest_timestamp
for file in cache_files: for file in cache_files:
if self.camera_name in file: if self.camera_name in file:
basename = os.path.splitext(file)[0] basename = os.path.splitext(file)[0]
_, date = basename.rsplit("-", maxsplit=1) _, date = basename.rsplit("-", maxsplit=1)
ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").astimezone( ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").timestamp()
datetime.timezone.utc
)
if ts > newest_segment_timestamp: if ts > newest_segment_timestamp:
newest_segment_timestamp = ts newest_segment_timestamp = ts
@@ -528,7 +525,7 @@ def process_frames(
fps = process_info["process_fps"] fps = process_info["process_fps"]
detection_fps = process_info["detection_fps"] detection_fps = process_info["detection_fps"]
current_frame_time = process_info["detection_frame"] current_frame_time = process_info["detection_frame"]
next_region_update = get_tomorrow_at_time(2) next_region_update = get_tomorrow_at_2()
fps_tracker = EventsPerSecond() fps_tracker = EventsPerSecond()
fps_tracker.start() fps_tracker.start()
@@ -550,7 +547,7 @@ def process_frames(
except queue.Empty: except queue.Empty:
logger.error(f"Unable to get updated region grid for {camera_name}") logger.error(f"Unable to get updated region grid for {camera_name}")
next_region_update = get_tomorrow_at_time(2) next_region_update = get_tomorrow_at_2()
try: try:
if exit_on_empty: if exit_on_empty:

301
web/package-lock.json generated
View File

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

View File

@@ -7,7 +7,6 @@ import axios from 'axios';
axios.defaults.baseURL = `${baseUrl}api/`; axios.defaults.baseURL = `${baseUrl}api/`;
axios.defaults.headers.common = { axios.defaults.headers.common = {
'X-CSRF-TOKEN': 1, 'X-CSRF-TOKEN': 1,
'X-CACHE-BYPASS': 1,
}; };
export function ApiProvider({ children, options }) { export function ApiProvider({ children, options }) {

View File

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

View File

@@ -1,19 +0,0 @@
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);

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import Dialog from '../components/Dialog';
import TimeAgo from '../components/TimeAgo'; import TimeAgo from '../components/TimeAgo';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { About } from '../icons/About'; import { About } from '../icons/About';
import { WebUI } from '../icons/WebUI';
const emptyObject = Object.freeze({}); const emptyObject = Object.freeze({});
@@ -348,17 +347,7 @@ export default function System() {
> >
<div className="capitalize text-lg flex justify-between p-4"> <div className="capitalize text-lg flex justify-between p-4">
<Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link> <Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
<div className="flex"> <Button onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
{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>
<div className="p-2"> <div className="p-2">
<Table className="w-full"> <Table className="w-full">