mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-10-04 07:06:58 +08:00
Compare commits
1 Commits
v0.13.0-be
...
release_wo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1d58e419f4 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -79,15 +79,6 @@ jobs:
|
||||
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
|
||||
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
||||
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
|
||||
- name: Build and push RockChip build
|
||||
uses: docker/bake-action@v3
|
||||
with:
|
||||
push: true
|
||||
targets: rk
|
||||
files: docker/rockchip/rk.hcl
|
||||
set: |
|
||||
rk.tags=${{ steps.setup.outputs.image-name }}-rk
|
||||
*.cache-from=type=gha
|
||||
jetson_jp4_build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Jetson Jetpack 4
|
||||
@@ -150,7 +141,7 @@ jobs:
|
||||
- arm64_build
|
||||
steps:
|
||||
- id: lowercaseRepo
|
||||
uses: ASzc/change-string-case-action@v6
|
||||
uses: ASzc/change-string-case-action@v5
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
|
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: lowercaseRepo
|
||||
uses: ASzc/change-string-case-action@v6
|
||||
uses: ASzc/change-string-case-action@v5
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
@@ -22,9 +22,8 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create tag variables
|
||||
run: |
|
||||
BRANCH=$([[ "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "master" || echo "dev")
|
||||
echo "BASE=ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}" >> $GITHUB_ENV
|
||||
echo "BUILD_TAG=${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
|
||||
- name: Tag and push the main image
|
||||
run: |
|
||||
|
@@ -2,5 +2,3 @@
|
||||
/docker/tensorrt/ @madsciencetist @NateMeyer
|
||||
/docker/tensorrt/*arm64* @madsciencetist
|
||||
/docker/tensorrt/*jetson* @madsciencetist
|
||||
|
||||
/docker/rockchip/ @MarcA711
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
NGINX_VERSION="1.25.3"
|
||||
NGINX_VERSION="1.25.2"
|
||||
VOD_MODULE_VERSION="1.31"
|
||||
SECURE_TOKEN_MODULE_VERSION="1.5"
|
||||
RTMP_MODULE_VERSION="1.2.2"
|
||||
|
@@ -13,9 +13,9 @@ psutil == 5.9.*
|
||||
pydantic == 1.10.*
|
||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||
PyYAML == 6.0.*
|
||||
pytz == 2023.3.post1
|
||||
ruamel.yaml == 0.18.*
|
||||
tzlocal == 5.2
|
||||
pytz == 2023.3
|
||||
ruamel.yaml == 0.17.*
|
||||
tzlocal == 5.1
|
||||
types-PyYAML == 6.0.*
|
||||
requests == 2.31.*
|
||||
types-requests == 2.31.*
|
||||
|
@@ -3,7 +3,6 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets"):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = Path(
|
||||
os.path.join("/run/secrets", secret_file)
|
||||
).read_text()
|
||||
|
||||
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
|
||||
|
||||
# Check if we can use .yaml instead of .yml
|
||||
|
@@ -32,8 +32,6 @@ http {
|
||||
gzip_proxied no-cache no-store private expired auth;
|
||||
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 {
|
||||
server 127.0.0.1:5001;
|
||||
keepalive 1024;
|
||||
@@ -187,19 +185,6 @@ http {
|
||||
proxy_pass http://frigate_api/;
|
||||
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 {
|
||||
access_log off;
|
||||
rewrite ^/api/(.*)$ $1 break;
|
||||
|
@@ -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/
|
@@ -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
|
@@ -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"]
|
||||
}
|
@@ -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.
@@ -31,7 +31,7 @@ First, set up a PTZ preset in your camera's firmware and give it a name. If you'
|
||||
|
||||
Edit your Frigate configuration file and enter the ONVIF parameters for your camera. Specify the object types to track, a required zone the object must enter to begin autotracking, and the camera preset name you configured in your camera's firmware to return to when tracking has ended. Optionally, specify a delay in seconds before Frigate returns the camera to the preset.
|
||||
|
||||
An [ONVIF connection](cameras.md) is required for autotracking to function. 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.
|
||||
|
||||
@@ -113,7 +113,7 @@ If you initially calibrate with zooming disabled and then enable zooming at a la
|
||||
|
||||
Every PTZ camera is different, so autotracking may not perform ideally in every situation. This experimental feature was initially developed using an EmpireTech/Dahua SD1A404XB-GNR.
|
||||
|
||||
The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 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.
|
||||
|
||||
|
@@ -13,6 +13,7 @@ See [the hwaccel docs](/configuration/hardware_acceleration.md) for more info on
|
||||
|
||||
| Preset | Usage | Other Notes |
|
||||
| --------------------- | ------------------------------ | ----------------------------------------------------- |
|
||||
| preset-rpi-32-h264 | 32 bit Rpi with h264 stream | |
|
||||
| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | |
|
||||
| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen |
|
||||
| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead |
|
||||
|
@@ -75,11 +75,11 @@ mqtt:
|
||||
# NOTE: must be unique if you are running multiple instances
|
||||
client_id: frigate
|
||||
# Optional: user
|
||||
# NOTE: MQTT user can be specified with an environment variables 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}'
|
||||
user: mqtt_user
|
||||
# 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}'
|
||||
password: password
|
||||
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
|
||||
@@ -231,8 +231,6 @@ detect:
|
||||
fps: 5
|
||||
# Optional: enables detection for the camera (default: 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)
|
||||
max_disappeared: 25
|
||||
# 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.
|
||||
inputs:
|
||||
# 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
|
||||
# Required: list of roles for this stream. valid values are: audio,detect,record,rtmp
|
||||
# NOTICE: In addition to assigning the audio, record, and rtmp roles,
|
||||
@@ -520,9 +518,6 @@ cameras:
|
||||
# to be replaced by a newer image. (default: shown below)
|
||||
best_image_timeout: 60
|
||||
|
||||
# Optional: URL to visit the camera web UI directly from the system page. Might not be available on every camera.
|
||||
webui_url: ""
|
||||
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
|
@@ -5,7 +5,7 @@ title: Object Detectors
|
||||
|
||||
# Officially Supported Detectors
|
||||
|
||||
Frigate provides the following builtin detector types: `cpu`, `edgetpu`, `openvino`, `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)
|
||||
|
||||
@@ -291,38 +291,3 @@ To verify that the integration is working correctly, start Frigate and observe t
|
||||
|
||||
|
||||
# Community Supported Detectors
|
||||
|
||||
## Rockchip RKNN-Toolkit-Lite2
|
||||
|
||||
This detector is only available if one of the following Rockchip SoCs is used:
|
||||
- RK3566/RK3568
|
||||
- RK3588/RK3588S
|
||||
- RV1103/RV1106
|
||||
- RK3562
|
||||
|
||||
These SoCs come with a NPU that will highly speed up detection.
|
||||
|
||||
### Setup
|
||||
|
||||
RKNN support is provided using the `-rk` suffix for the docker image. Moreover, privileged mode must be enabled by adding the `--privileged` flag to your docker run command or `privileged: true` to your `docker-compose.yml` file.
|
||||
|
||||
### Configuration
|
||||
|
||||
This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for one). Lines that are required at least to use the detector are labeled as required, all other lines are optional.
|
||||
```yaml
|
||||
detectors: # required
|
||||
rknn: # required
|
||||
type: rknn # required
|
||||
|
||||
model: # required
|
||||
# path to .rknn model file
|
||||
path: /models/yolov8n-320x320.rknn
|
||||
# width and height of detection frames
|
||||
width: 320
|
||||
height: 320
|
||||
# pixel format of detection frame
|
||||
# default value is rgb but yolov models usually use bgr format
|
||||
input_pixel_format: bgr # required
|
||||
# shape of detection frame
|
||||
input_tensor: nhwc
|
||||
```
|
@@ -95,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.
|
||||
|
||||
#### Rockchip SoC
|
||||
|
||||
Frigate supports SBCs with the following Rockchip SoCs:
|
||||
- RK3566/RK3568
|
||||
- RK3588/RK3588S
|
||||
- RV1103/RV1106
|
||||
- RK3562
|
||||
|
||||
Using the yolov8n model and an Orange Pi 5 Plus with RK3588 SoC inference speeds vary between 25-40 ms.
|
||||
|
||||
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
||||
|
||||
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
||||
|
@@ -95,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-jp4` - Frigate build optimized for nvidia Jetson devices running Jetpack 4.6
|
||||
`ghcr.io/blakeblackshear/frigate:stable-rk` - Frigate build for SBCs with Rockchip SoC
|
||||
|
||||
:::
|
||||
|
||||
|
@@ -263,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.
|
||||
|
||||
### `GET /api/<camera_name>/grid.jpg`
|
||||
|
||||
Returns the latest camera image with the regions grid overlaid.
|
||||
|
||||
### `GET /clips/<camera>-<id>.jpg`
|
||||
|
||||
JPG snapshot for the given camera and event id.
|
||||
@@ -365,7 +361,3 @@ Recording retention config still applies to manual events, if frigate is configu
|
||||
### `PUT /api/events/<event_id>/end`
|
||||
|
||||
End a specific manual event without a predetermined length.
|
||||
|
||||
### `POST /api/restart`
|
||||
|
||||
Restarts Frigate process.
|
||||
|
@@ -221,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`.
|
||||
|
||||
### `frigate/<camera_name>/ptz_autotracker/active`
|
||||
|
||||
Topic to determine if PTZ autotracker is actively tracking an object. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/birdseye/set`
|
||||
|
||||
Topic to turn Birdseye for a camera on and off. Expected values are `ON` and `OFF`. Birdseye mode
|
||||
|
@@ -19,7 +19,7 @@ Once logged in, you can generate an API key for Frigate in Settings.
|
||||
|
||||
### Set your API key
|
||||
|
||||
In Frigate, you can 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
|
||||
|
||||
|
@@ -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).
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### 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?
|
||||
|
||||
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?
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
@@ -191,8 +191,7 @@ class FrigateApp:
|
||||
"i",
|
||||
self.config.cameras[camera_name].onvif.autotracking.enabled,
|
||||
),
|
||||
"ptz_tracking_active": mp.Event(),
|
||||
"ptz_motor_stopped": mp.Event(),
|
||||
"ptz_stopped": mp.Event(),
|
||||
"ptz_reset": mp.Event(),
|
||||
"ptz_start_time": mp.Value("d", 0.0), # type: ignore[typeddict-item]
|
||||
# issue https://github.com/python/typeshed/issues/8799
|
||||
@@ -213,7 +212,7 @@ class FrigateApp:
|
||||
# issue https://github.com/python/typeshed/issues/8799
|
||||
# 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] = {
|
||||
"audio_enabled": mp.Value( # type: ignore[typeddict-item]
|
||||
# issue https://github.com/python/typeshed/issues/8799
|
||||
@@ -445,7 +444,6 @@ class FrigateApp:
|
||||
self.config,
|
||||
self.onvif_controller,
|
||||
self.ptz_metrics,
|
||||
self.dispatcher,
|
||||
self.stop_event,
|
||||
)
|
||||
self.ptz_autotracker_thread.start()
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""Websocket communicator."""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
@@ -13,7 +12,7 @@ from ws4py.server.wsgirefserver import (
|
||||
WSGIServer,
|
||||
)
|
||||
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.config import FrigateConfig
|
||||
@@ -21,18 +20,6 @@ from frigate.config import FrigateConfig
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocket(WebSocket_):
|
||||
def unhandled_error(self, error):
|
||||
"""
|
||||
Handles the unfriendly socket closures on the server side
|
||||
without showing a confusing error message
|
||||
"""
|
||||
if hasattr(error, "errno") and error.errno == errno.ECONNRESET:
|
||||
pass
|
||||
else:
|
||||
logging.getLogger("ws4py").exception("Failed to receive data")
|
||||
|
||||
|
||||
class WebSocketClient(Communicator): # type: ignore[misc]
|
||||
"""Frigate wrapper for ws client."""
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
@@ -48,13 +47,6 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
|
||||
# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets"):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = Path(
|
||||
os.path.join("/run/secrets", secret_file)
|
||||
).read_text()
|
||||
|
||||
DEFAULT_TRACKED_OBJECTS = ["person"]
|
||||
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
|
||||
@@ -179,7 +171,7 @@ class PtzAutotrackConfig(FrigateBaseModel):
|
||||
timeout: int = Field(
|
||||
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=[],
|
||||
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."
|
||||
)
|
||||
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(
|
||||
title="Maximum number of frames the object can dissapear before detection ends."
|
||||
)
|
||||
@@ -739,9 +728,6 @@ class CameraConfig(FrigateBaseModel):
|
||||
default=60,
|
||||
title="How long to wait for the image with the highest confidence score.",
|
||||
)
|
||||
webui_url: Optional[str] = Field(
|
||||
title="URL to visit the camera directly from system page",
|
||||
)
|
||||
zones: Dict[str, ZoneConfig] = Field(
|
||||
default_factory=dict, title="Zone configuration."
|
||||
)
|
||||
@@ -1157,11 +1143,6 @@ class FrigateConfig(FrigateBaseModel):
|
||||
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
|
||||
max_disappeared = camera_config.detect.fps * 5
|
||||
if camera_config.detect.max_disappeared is None:
|
||||
|
@@ -60,7 +60,7 @@ REQUEST_REGION_GRID = "request_region_grid"
|
||||
|
||||
# Autotracking
|
||||
|
||||
AUTOTRACKING_MAX_AREA_RATIO = 0.6
|
||||
AUTOTRACKING_MAX_AREA_RATIO = 0.5
|
||||
AUTOTRACKING_MOTION_MIN_DISTANCE = 20
|
||||
AUTOTRACKING_MOTION_MAX_POINTS = 500
|
||||
AUTOTRACKING_MAX_MOVE_METRICS = 500
|
||||
|
@@ -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])
|
@@ -293,16 +293,6 @@ class TensorRtDetector(DetectionApi):
|
||||
# raw_detections: Nx7 numpy arrays of
|
||||
# [[x, y, w, h, box_confidence, class_id, class_prob],
|
||||
|
||||
# throw out any detections with negative class IDs
|
||||
valid_detections = []
|
||||
for r in raw_detections:
|
||||
if r[5] >= 0:
|
||||
valid_detections.append(r)
|
||||
else:
|
||||
logger.warning(f"Found TensorRT detection with invalid class id {r}")
|
||||
|
||||
raw_detections = valid_detections
|
||||
|
||||
# Calculate score as box_confidence x class_prob
|
||||
raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6]
|
||||
# Reorder elements by the score, best on top, remove class_prob
|
||||
@@ -313,7 +303,6 @@ class TensorRtDetector(DetectionApi):
|
||||
ordered[:, 3] = np.clip(ordered[:, 3] + ordered[:, 1], 0, 1)
|
||||
# put result into the correct order and limit to top 20
|
||||
detections = ordered[:, [5, 4, 1, 0, 3, 2]][:20]
|
||||
|
||||
# pad to 20x6 shape
|
||||
append_cnt = 20 - len(detections)
|
||||
if append_cnt > 0:
|
||||
|
@@ -240,10 +240,7 @@ class AudioEventMaintainer(threading.Thread):
|
||||
rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float))))
|
||||
|
||||
# Transform RMS to dBFS (decibels relative to full scale)
|
||||
if rms > 0:
|
||||
dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE)
|
||||
else:
|
||||
dBFS = 0
|
||||
dBFS = 20 * np.log10(np.abs(rms) / AUDIO_MAX_BIT_RANGE)
|
||||
|
||||
self.inter_process_communicator.queue.put(
|
||||
(f"{self.config.name}/audio/dBFS", float(dBFS))
|
||||
|
@@ -106,10 +106,10 @@ class ExternalEventProcessor:
|
||||
# write jpg snapshot with optional annotations
|
||||
if draw.get("boxes") and isinstance(draw.get("boxes"), list):
|
||||
for box in draw.get("boxes"):
|
||||
x = int(box["box"][0] * camera_config.detect.width)
|
||||
y = int(box["box"][1] * camera_config.detect.height)
|
||||
width = int(box["box"][2] * camera_config.detect.width)
|
||||
height = int(box["box"][3] * camera_config.detect.height)
|
||||
x = box["box"][0] * camera_config.detect.width
|
||||
y = box["box"][1] * camera_config.detect.height
|
||||
width = box["box"][2] * camera_config.detect.width
|
||||
height = box["box"][3] * camera_config.detect.height
|
||||
|
||||
draw_box_with_label(
|
||||
img_frame,
|
||||
|
@@ -55,6 +55,7 @@ _user_agent_args = [
|
||||
]
|
||||
|
||||
PRESETS_HW_ACCEL_DECODE = {
|
||||
"preset-rpi-32-h264": "-c:v:1 h264_v4l2m2m",
|
||||
"preset-rpi-64-h264": "-c:v:1 h264_v4l2m2m",
|
||||
"preset-vaapi": f"-hwaccel_flags allow_profile_mismatch -hwaccel vaapi -hwaccel_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format vaapi",
|
||||
"preset-intel-qsv-h264": f"-hwaccel qsv -qsv_device {_gpu_selector.get_selected_gpu()} -hwaccel_output_format qsv -c:v h264_qsv",
|
||||
@@ -67,6 +68,7 @@ PRESETS_HW_ACCEL_DECODE = {
|
||||
}
|
||||
|
||||
PRESETS_HW_ACCEL_SCALE = {
|
||||
"preset-rpi-32-h264": "-r {0} -vf fps={0},scale={1}:{2}",
|
||||
"preset-rpi-64-h264": "-r {0} -vf fps={0},scale={1}:{2}",
|
||||
"preset-vaapi": "-r {0} -vf fps={0},scale_vaapi=w={1}:h={2},hwdownload,format=yuv420p",
|
||||
"preset-intel-qsv-h264": "-r {0} -vf vpp_qsv=framerate={0}:w={1}:h={2}:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
||||
@@ -79,6 +81,7 @@ PRESETS_HW_ACCEL_SCALE = {
|
||||
}
|
||||
|
||||
PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
|
||||
"preset-rpi-32-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
|
||||
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
|
||||
"preset-vaapi": "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {1}",
|
||||
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
@@ -91,7 +94,8 @@ PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = {
|
||||
}
|
||||
|
||||
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-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
|
139
frigate/http.py
139
frigate/http.py
@@ -41,7 +41,7 @@ from frigate.const import (
|
||||
RECORD_DIR,
|
||||
)
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.models import Event, Recordings, Regions, Timeline
|
||||
from frigate.models import Event, Recordings, Timeline
|
||||
from frigate.object_processing import TrackedObject
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.ptz.onvif import OnvifController
|
||||
@@ -726,112 +726,6 @@ def label_snapshot(camera_name, label):
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/<camera_name>/grid.jpg")
|
||||
def grid_snapshot(camera_name):
|
||||
request.args.get("type", default="region")
|
||||
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
detect = current_app.frigate_config.cameras[camera_name].detect
|
||||
frame = current_app.detected_frames_processor.get_current_frame(camera_name, {})
|
||||
retry_interval = float(
|
||||
current_app.frigate_config.cameras.get(camera_name).ffmpeg.retry_interval
|
||||
or 10
|
||||
)
|
||||
|
||||
if frame is None or datetime.now().timestamp() > (
|
||||
current_app.detected_frames_processor.get_current_frame_time(camera_name)
|
||||
+ retry_interval
|
||||
):
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Unable to get valid frame"}),
|
||||
500,
|
||||
)
|
||||
|
||||
try:
|
||||
grid = (
|
||||
Regions.select(Regions.grid)
|
||||
.where(Regions.camera == camera_name)
|
||||
.get()
|
||||
.grid
|
||||
)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Unable to get region grid"}),
|
||||
500,
|
||||
)
|
||||
|
||||
grid_size = len(grid)
|
||||
grid_coef = 1.0 / grid_size
|
||||
width = detect.width
|
||||
height = detect.height
|
||||
for x in range(grid_size):
|
||||
for y in range(grid_size):
|
||||
cell = grid[x][y]
|
||||
|
||||
if len(cell["sizes"]) == 0:
|
||||
continue
|
||||
|
||||
std_dev = round(cell["std_dev"] * width, 2)
|
||||
mean = round(cell["mean"] * width, 2)
|
||||
cv2.rectangle(
|
||||
frame,
|
||||
(int(x * grid_coef * width), int(y * grid_coef * height)),
|
||||
(
|
||||
int((x + 1) * grid_coef * width),
|
||||
int((y + 1) * grid_coef * height),
|
||||
),
|
||||
(0, 255, 0),
|
||||
2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
f"#: {len(cell['sizes'])}",
|
||||
(
|
||||
int(x * grid_coef * width + 10),
|
||||
int((y * grid_coef + 0.02) * height),
|
||||
),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=0.5,
|
||||
color=(0, 255, 0),
|
||||
thickness=2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
f"std: {std_dev}",
|
||||
(
|
||||
int(x * grid_coef * width + 10),
|
||||
int((y * grid_coef + 0.05) * height),
|
||||
),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=0.5,
|
||||
color=(0, 255, 0),
|
||||
thickness=2,
|
||||
)
|
||||
cv2.putText(
|
||||
frame,
|
||||
f"avg: {mean}",
|
||||
(
|
||||
int(x * grid_coef * width + 10),
|
||||
int((y * grid_coef + 0.08) * height),
|
||||
),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=0.5,
|
||||
color=(0, 255, 0),
|
||||
thickness=2,
|
||||
)
|
||||
|
||||
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Camera not found"}),
|
||||
404,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/clip.mp4")
|
||||
def event_clip(id):
|
||||
download = request.args.get("download", type=bool)
|
||||
@@ -1052,7 +946,7 @@ def events():
|
||||
if is_submitted is not None:
|
||||
if is_submitted == 0:
|
||||
clauses.append((Event.plus_id.is_null()))
|
||||
elif is_submitted > 0:
|
||||
else:
|
||||
clauses.append((Event.plus_id != ""))
|
||||
|
||||
if len(clauses) == 0:
|
||||
@@ -1494,8 +1388,6 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
||||
)
|
||||
)
|
||||
.where(Recordings.camera == camera_name)
|
||||
.order_by(Recordings.start_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -2103,30 +1995,3 @@ def logs(service: str):
|
||||
jsonify({"success": False, "message": "Could not find log file"}),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/restart", methods=["POST"])
|
||||
def restart():
|
||||
try:
|
||||
restart_frigate()
|
||||
except Exception as e:
|
||||
logging.error(f"Error restarting Frigate: {e}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Unable to restart Frigate.",
|
||||
}
|
||||
),
|
||||
500,
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Restarting (this can take up to one minute)...",
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
@@ -248,8 +248,10 @@ class TrackedObject:
|
||||
if self.obj_data["frame_time"] - self.previous["frame_time"] > 60:
|
||||
significant_change = True
|
||||
|
||||
# update autotrack at most 3 objects per second
|
||||
if self.obj_data["frame_time"] - self.previous["frame_time"] >= (1 / 3):
|
||||
# update autotrack at half fps
|
||||
if self.obj_data["frame_time"] - self.previous["frame_time"] > (
|
||||
1 / (self.camera_config.detect.fps / 2)
|
||||
):
|
||||
autotracker_update = True
|
||||
|
||||
self.obj_data.update(obj_data)
|
||||
|
@@ -63,8 +63,8 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
|
||||
a_w, a_h = get_standard_aspect_ratio(width, height)
|
||||
|
||||
if round(a_w / a_h, 2) != round(width / height, 2):
|
||||
canvas_width = int(width // 4 * 4)
|
||||
canvas_height = int((canvas_width / a_w * a_h) // 4 * 4)
|
||||
canvas_width = width
|
||||
canvas_height = int((canvas_width / a_w) * a_h)
|
||||
logger.warning(
|
||||
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]:
|
||||
"""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."""
|
||||
candidate_layout = []
|
||||
starting_x = 0
|
||||
@@ -492,7 +492,7 @@ class BirdsEyeFrameManager:
|
||||
x + scaled_width > self.canvas.width
|
||||
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)))
|
||||
x += scaled_width
|
||||
@@ -564,24 +564,10 @@ class BirdsEyeFrameManager:
|
||||
return None
|
||||
|
||||
row_height = int(self.canvas.height / coefficient)
|
||||
total_width, total_height, standard_candidate_layout = map_layout(
|
||||
camera_layout, row_height
|
||||
)
|
||||
total_width, total_height, standard_candidate_layout = map_layout(row_height)
|
||||
|
||||
if not standard_candidate_layout:
|
||||
# if standard layout didn't work
|
||||
# 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
|
||||
return None
|
||||
|
||||
# layout can't be optimized more
|
||||
if total_width / self.canvas.width >= 0.99:
|
||||
@@ -592,7 +578,7 @@ class BirdsEyeFrameManager:
|
||||
1 / (total_height / self.canvas.height),
|
||||
)
|
||||
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:
|
||||
return scaled_layout
|
||||
|
@@ -3,7 +3,6 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
|
||||
import cv2
|
||||
@@ -37,10 +36,6 @@ class PlusApi:
|
||||
self.key = None
|
||||
if PLUS_ENV_VAR in os.environ:
|
||||
self.key = os.environ.get(PLUS_ENV_VAR)
|
||||
elif os.path.isdir("/run/secrets") and PLUS_ENV_VAR in os.listdir(
|
||||
"/run/secrets"
|
||||
):
|
||||
self.key = Path(os.path.join("/run/secrets", PLUS_ENV_VAR)).read_text()
|
||||
# check for the addon options file
|
||||
elif os.path.isfile("/data/options.json"):
|
||||
with open("/data/options.json") as f:
|
||||
|
@@ -18,7 +18,6 @@ from norfair.camera_motion import (
|
||||
TranslationTransformationGetter,
|
||||
)
|
||||
|
||||
from frigate.comms.dispatcher import Dispatcher
|
||||
from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum
|
||||
from frigate.const import (
|
||||
AUTOTRACKING_MAX_AREA_RATIO,
|
||||
@@ -145,12 +144,11 @@ class PtzAutoTrackerThread(threading.Thread):
|
||||
config: FrigateConfig,
|
||||
onvif: OnvifController,
|
||||
ptz_metrics: dict[str, PTZMetricsTypes],
|
||||
dispatcher: Dispatcher,
|
||||
stop_event: MpEvent,
|
||||
) -> None:
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "ptz_autotracker"
|
||||
self.ptz_autotracker = PtzAutoTracker(config, onvif, ptz_metrics, dispatcher)
|
||||
self.ptz_autotracker = PtzAutoTracker(config, onvif, ptz_metrics)
|
||||
self.stop_event = stop_event
|
||||
self.config = config
|
||||
|
||||
@@ -177,12 +175,10 @@ class PtzAutoTracker:
|
||||
config: FrigateConfig,
|
||||
onvif: OnvifController,
|
||||
ptz_metrics: PTZMetricsTypes,
|
||||
dispatcher: Dispatcher,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.onvif = onvif
|
||||
self.ptz_metrics = ptz_metrics
|
||||
self.dispatcher = dispatcher
|
||||
self.tracked_object: dict[str, object] = {}
|
||||
self.tracked_object_history: dict[str, object] = {}
|
||||
self.tracked_object_metrics: dict[str, object] = {}
|
||||
@@ -219,8 +215,8 @@ class PtzAutoTracker:
|
||||
maxlen=round(camera_config.detect.fps * 1.5)
|
||||
)
|
||||
self.tracked_object_metrics[camera] = {
|
||||
"max_target_box": AUTOTRACKING_MAX_AREA_RATIO
|
||||
** (1 / self.zoom_factor[camera])
|
||||
"max_target_box": 1
|
||||
- (AUTOTRACKING_MAX_AREA_RATIO ** self.zoom_factor[camera])
|
||||
}
|
||||
|
||||
self.calibrating[camera] = False
|
||||
@@ -272,10 +268,6 @@ class PtzAutoTracker:
|
||||
|
||||
if camera_config.onvif.autotracking.movement_weights:
|
||||
if len(camera_config.onvif.autotracking.movement_weights) == 5:
|
||||
camera_config.onvif.autotracking.movement_weights = [
|
||||
float(val)
|
||||
for val in camera_config.onvif.autotracking.movement_weights
|
||||
]
|
||||
self.ptz_metrics[camera][
|
||||
"ptz_min_zoom"
|
||||
].value = camera_config.onvif.autotracking.movement_weights[0]
|
||||
@@ -298,8 +290,6 @@ class PtzAutoTracker:
|
||||
if camera_config.onvif.autotracking.calibrate_on_startup:
|
||||
self._calibrate_camera(camera)
|
||||
|
||||
self.ptz_metrics[camera]["ptz_tracking_active"].clear()
|
||||
self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False)
|
||||
self.autotracker_init[camera] = True
|
||||
|
||||
def _write_config(self, camera):
|
||||
@@ -344,7 +334,7 @@ class PtzAutoTracker:
|
||||
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)
|
||||
|
||||
zoom_out_values.append(self.ptz_metrics[camera]["ptz_zoom_level"].value)
|
||||
@@ -355,7 +345,7 @@ class PtzAutoTracker:
|
||||
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)
|
||||
|
||||
zoom_in_values.append(self.ptz_metrics[camera]["ptz_zoom_level"].value)
|
||||
@@ -373,7 +363,7 @@ class PtzAutoTracker:
|
||||
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)
|
||||
|
||||
zoom_out_values.append(
|
||||
@@ -389,7 +379,7 @@ class PtzAutoTracker:
|
||||
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)
|
||||
|
||||
zoom_in_values.append(
|
||||
@@ -412,10 +402,10 @@ class PtzAutoTracker:
|
||||
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
|
||||
)
|
||||
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
|
||||
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)
|
||||
|
||||
for step in range(num_steps):
|
||||
@@ -426,7 +416,7 @@ class PtzAutoTracker:
|
||||
self.onvif._move_relative(camera, pan, tilt, 0, 1)
|
||||
|
||||
# Wait until the camera finishes moving
|
||||
while not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
|
||||
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
|
||||
self.onvif.get_camera_status(camera)
|
||||
stop_time = time.time()
|
||||
|
||||
@@ -444,10 +434,10 @@ class PtzAutoTracker:
|
||||
self.config.cameras[camera].onvif.autotracking.return_preset.lower(),
|
||||
)
|
||||
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
|
||||
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)
|
||||
|
||||
logger.info(
|
||||
@@ -531,11 +521,7 @@ class PtzAutoTracker:
|
||||
camera_height = camera_config.frame_shape[0]
|
||||
|
||||
# Extract areas and calculate weighted average
|
||||
# grab the largest dimension of the bounding box and create a square from that
|
||||
areas = [
|
||||
max(obj["box"][2] - obj["box"][0], obj["box"][3] - obj["box"][1]) ** 2
|
||||
for obj in self.tracked_object_history[camera]
|
||||
]
|
||||
areas = [obj["area"] for obj in self.tracked_object_history[camera]]
|
||||
|
||||
filtered_areas = (
|
||||
remove_outliers(areas)
|
||||
@@ -612,9 +598,7 @@ class PtzAutoTracker:
|
||||
self.onvif._move_relative(camera, pan, tilt, 0, 1)
|
||||
|
||||
# Wait until the camera finishes moving
|
||||
while not self.ptz_metrics[camera][
|
||||
"ptz_motor_stopped"
|
||||
].is_set():
|
||||
while not self.ptz_metrics[camera]["ptz_stopped"].is_set():
|
||||
self.onvif.get_camera_status(camera)
|
||||
|
||||
if (
|
||||
@@ -624,7 +608,7 @@ class PtzAutoTracker:
|
||||
self.onvif._zoom_absolute(camera, zoom, 1)
|
||||
|
||||
# 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)
|
||||
|
||||
if self.config.cameras[camera].onvif.autotracking.movement_weights:
|
||||
@@ -702,20 +686,19 @@ class PtzAutoTracker:
|
||||
camera_height = camera_config.frame_shape[0]
|
||||
camera_fps = camera_config.detect.fps
|
||||
|
||||
# estimate_velocity is a numpy array of bbox top,left and bottom,right velocities
|
||||
velocities = obj.obj_data["estimate_velocity"]
|
||||
logger.debug(f"{camera}: Velocities from norfair: {velocities}")
|
||||
|
||||
# if we are close enough to zero, return right away
|
||||
if np.all(np.round(velocities) == 0):
|
||||
return True, np.zeros((4,))
|
||||
return True, np.zeros((2, 2))
|
||||
|
||||
# Thresholds
|
||||
x_mags_thresh = camera_width / camera_fps / 2
|
||||
y_mags_thresh = camera_height / camera_fps / 2
|
||||
dir_thresh = 0.93
|
||||
delta_thresh = 20
|
||||
var_thresh = 10
|
||||
delta_thresh = 12
|
||||
var_thresh = 5
|
||||
|
||||
# Check magnitude
|
||||
x_mags = np.abs(velocities[:, 0])
|
||||
@@ -739,6 +722,7 @@ class PtzAutoTracker:
|
||||
np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1])
|
||||
)
|
||||
dir_thresh = 0.6 if np.all(delta < delta_thresh / 2) else dir_thresh
|
||||
print(f"cosine sim: {cosine_sim}")
|
||||
invalid_dirs = cosine_sim < dir_thresh
|
||||
|
||||
# Combine
|
||||
@@ -768,10 +752,10 @@ class PtzAutoTracker:
|
||||
)
|
||||
)
|
||||
# invalid velocity
|
||||
return False, np.zeros((4,))
|
||||
return False, np.zeros((2, 2))
|
||||
else:
|
||||
logger.debug(f"{camera}: Valid velocity ")
|
||||
return True, velocities.flatten()
|
||||
return True, np.mean(velocities, axis=0)
|
||||
|
||||
def _get_distance_threshold(self, camera, obj):
|
||||
# Returns true if Euclidean distance from object to center of frame is
|
||||
@@ -852,7 +836,7 @@ class PtzAutoTracker:
|
||||
# ensure object is not moving quickly
|
||||
below_velocity_threshold = np.all(
|
||||
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)
|
||||
|
||||
below_area_threshold = (
|
||||
@@ -954,7 +938,7 @@ class PtzAutoTracker:
|
||||
camera_height = camera_config.frame_shape[0]
|
||||
camera_fps = camera_config.detect.fps
|
||||
|
||||
average_velocity = np.zeros((4,))
|
||||
average_velocity = np.zeros((2, 2))
|
||||
predicted_box = obj.obj_data["box"]
|
||||
|
||||
centroid_x = obj.obj_data["centroid"][0]
|
||||
@@ -982,6 +966,7 @@ class PtzAutoTracker:
|
||||
# this box could exceed the frame boundaries if velocity is high
|
||||
# but we'll handle that in _enqueue_move() as two separate moves
|
||||
current_box = np.array(obj.obj_data["box"])
|
||||
average_velocity = np.tile(average_velocity, 2)
|
||||
predicted_box = (
|
||||
current_box
|
||||
+ camera_fps * predicted_movement_time * average_velocity
|
||||
@@ -1025,10 +1010,7 @@ class PtzAutoTracker:
|
||||
zoom = 0
|
||||
result = None
|
||||
current_zoom_level = self.ptz_metrics[camera]["ptz_zoom_level"].value
|
||||
target_box = max(
|
||||
obj.obj_data["box"][2] - obj.obj_data["box"][0],
|
||||
obj.obj_data["box"][3] - obj.obj_data["box"][1],
|
||||
) ** 2 / (camera_width * camera_height)
|
||||
target_box = obj.obj_data["area"] / (camera_width * camera_height)
|
||||
|
||||
# absolute zooming separately from pan/tilt
|
||||
if camera_config.onvif.autotracking.zooming == ZoomingModeEnum.absolute:
|
||||
@@ -1079,15 +1061,24 @@ class PtzAutoTracker:
|
||||
< 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 = (ratio - 1) / (ratio + 1)
|
||||
zoom = (
|
||||
2
|
||||
* (
|
||||
limit
|
||||
/ (
|
||||
self.tracked_object_metrics[camera]["target_box"]
|
||||
+ limit
|
||||
)
|
||||
)
|
||||
- 1
|
||||
)
|
||||
logger.debug(f"{camera}: Zoom calculation: {zoom}")
|
||||
if not result:
|
||||
# zoom out with special condition if zooming out because of velocity, edges, etc.
|
||||
zoom = -(1 - zoom) if zoom > 0 else -(zoom * 2 + 1)
|
||||
zoom = -(1 - zoom) if zoom > 0 else -(zoom + 1)
|
||||
if result:
|
||||
# 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}")
|
||||
|
||||
@@ -1126,10 +1117,6 @@ class PtzAutoTracker:
|
||||
logger.debug(
|
||||
f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}"
|
||||
)
|
||||
self.ptz_metrics[camera]["ptz_tracking_active"].set()
|
||||
self.dispatcher.publish(
|
||||
f"{camera}/ptz_autotracker/active", "ON", retain=False
|
||||
)
|
||||
self.tracked_object[camera] = obj
|
||||
|
||||
self.tracked_object_history[camera].append(copy.deepcopy(obj.obj_data))
|
||||
@@ -1212,8 +1199,8 @@ class PtzAutoTracker:
|
||||
)
|
||||
self.tracked_object[camera] = None
|
||||
self.tracked_object_metrics[camera] = {
|
||||
"max_target_box": AUTOTRACKING_MAX_AREA_RATIO
|
||||
** (1 / self.zoom_factor[camera])
|
||||
"max_target_box": 1
|
||||
- (AUTOTRACKING_MAX_AREA_RATIO ** self.zoom_factor[camera])
|
||||
}
|
||||
|
||||
def camera_maintenance(self, camera):
|
||||
@@ -1232,7 +1219,7 @@ class PtzAutoTracker:
|
||||
if not self.autotracker_init[camera]:
|
||||
self._autotracker_setup(self.config.cameras[camera], camera)
|
||||
# regularly update camera status
|
||||
if not self.ptz_metrics[camera]["ptz_motor_stopped"].is_set():
|
||||
if not self.ptz_metrics[camera]["ptz_stopped"].is_set():
|
||||
self.onvif.get_camera_status(camera)
|
||||
|
||||
# return to preset if tracking is over
|
||||
@@ -1255,7 +1242,7 @@ class PtzAutoTracker:
|
||||
while not self.move_queues[camera].empty():
|
||||
self.move_queues[camera].get()
|
||||
|
||||
self.ptz_metrics[camera]["ptz_motor_stopped"].wait()
|
||||
self.ptz_metrics[camera]["ptz_stopped"].wait()
|
||||
logger.debug(
|
||||
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
|
||||
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.ptz_metrics[camera]["ptz_tracking_active"].clear()
|
||||
self.dispatcher.publish(
|
||||
f"{camera}/ptz_autotracker/active", "OFF", retain=False
|
||||
)
|
||||
self.ptz_metrics[camera]["ptz_reset"].set()
|
||||
|
@@ -299,7 +299,7 @@ class OnvifController:
|
||||
return
|
||||
|
||||
self.cams[camera_name]["active"] = True
|
||||
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
|
||||
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
|
||||
logger.debug(
|
||||
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
|
||||
)
|
||||
@@ -366,7 +366,7 @@ class OnvifController:
|
||||
return
|
||||
|
||||
self.cams[camera_name]["active"] = True
|
||||
self.ptz_metrics[camera_name]["ptz_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_stop_time"].value = 0
|
||||
move_request = self.cams[camera_name]["move_request"]
|
||||
@@ -413,7 +413,7 @@ class OnvifController:
|
||||
return
|
||||
|
||||
self.cams[camera_name]["active"] = True
|
||||
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
|
||||
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
|
||||
logger.debug(
|
||||
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
|
||||
)
|
||||
@@ -543,8 +543,8 @@ class OnvifController:
|
||||
zoom_status is None or zoom_status.lower() == "idle"
|
||||
):
|
||||
self.cams[camera_name]["active"] = False
|
||||
if not self.ptz_metrics[camera_name]["ptz_motor_stopped"].is_set():
|
||||
self.ptz_metrics[camera_name]["ptz_motor_stopped"].set()
|
||||
if not self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
|
||||
self.ptz_metrics[camera_name]["ptz_stopped"].set()
|
||||
|
||||
logger.debug(
|
||||
f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
|
||||
@@ -555,8 +555,8 @@ class OnvifController:
|
||||
]["ptz_frame_time"].value
|
||||
else:
|
||||
self.cams[camera_name]["active"] = True
|
||||
if self.ptz_metrics[camera_name]["ptz_motor_stopped"].is_set():
|
||||
self.ptz_metrics[camera_name]["ptz_motor_stopped"].clear()
|
||||
if self.ptz_metrics[camera_name]["ptz_stopped"].is_set():
|
||||
self.ptz_metrics[camera_name]["ptz_stopped"].clear()
|
||||
|
||||
logger.debug(
|
||||
f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name]['ptz_frame_time'].value}"
|
||||
@@ -586,7 +586,7 @@ class OnvifController:
|
||||
|
||||
# some hikvision cams won't update MoveStatus, so warn if it hasn't changed
|
||||
if (
|
||||
not self.ptz_metrics[camera_name]["ptz_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 self.ptz_metrics[camera_name]["ptz_start_time"].value != 0
|
||||
and self.ptz_metrics[camera_name]["ptz_frame_time"].value
|
||||
|
@@ -3,15 +3,17 @@
|
||||
import datetime
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from multiprocessing.synchronize import Event as MpEvent
|
||||
from pathlib import Path
|
||||
|
||||
from peewee import DatabaseError, chunked
|
||||
|
||||
from frigate.config import FrigateConfig, RetainModeEnum
|
||||
from frigate.const import CACHE_DIR, RECORD_DIR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.record.util import remove_empty_directories, sync_recordings
|
||||
from frigate.util.builtin import get_tomorrow_at_time
|
||||
from frigate.models import Event, Recordings, RecordingsToDelete
|
||||
from frigate.record.util import remove_empty_directories
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -178,25 +180,76 @@ class RecordingCleanup(threading.Thread):
|
||||
logger.debug("End all cameras.")
|
||||
logger.debug("End expire recordings.")
|
||||
|
||||
def sync_recordings(self) -> None:
|
||||
"""Check the db for stale recordings entries that don't exist in the filesystem."""
|
||||
logger.debug("Start sync recordings.")
|
||||
|
||||
# get all recordings in the db
|
||||
recordings = Recordings.select(Recordings.id, Recordings.path)
|
||||
|
||||
# get all recordings files on disk and put them in a set
|
||||
files_on_disk = {
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(RECORD_DIR)
|
||||
for file in files
|
||||
}
|
||||
|
||||
# Use pagination to process records in chunks
|
||||
page_size = 1000
|
||||
num_pages = (recordings.count() + page_size - 1) // page_size
|
||||
recordings_to_delete = set()
|
||||
|
||||
for page in range(num_pages):
|
||||
for recording in recordings.paginate(page, page_size):
|
||||
if recording.path not in files_on_disk:
|
||||
recordings_to_delete.add(recording.id)
|
||||
|
||||
# convert back to list of dictionaries for insertion
|
||||
recordings_to_delete = [
|
||||
{"id": recording_id} for recording_id in recordings_to_delete
|
||||
]
|
||||
|
||||
if len(recordings_to_delete) / max(1, recordings.count()) > 0.5:
|
||||
logger.debug(
|
||||
f"Deleting {(len(recordings_to_delete) / recordings.count()):2f}% of recordings could be due to configuration error. Aborting..."
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(
|
||||
f"Deleting {len(recordings_to_delete)} recordings with missing files"
|
||||
)
|
||||
|
||||
# create a temporary table for deletion
|
||||
RecordingsToDelete.create_table(temporary=True)
|
||||
|
||||
# insert ids to the temporary table
|
||||
max_inserts = 1000
|
||||
for batch in chunked(recordings_to_delete, max_inserts):
|
||||
RecordingsToDelete.insert_many(batch).execute()
|
||||
|
||||
try:
|
||||
# delete records in the main table that exist in the temporary table
|
||||
query = Recordings.delete().where(
|
||||
Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id))
|
||||
)
|
||||
query.execute()
|
||||
except DatabaseError as e:
|
||||
logger.error(f"Database error during delete: {e}")
|
||||
|
||||
logger.debug("End sync recordings.")
|
||||
|
||||
def run(self) -> None:
|
||||
# on startup sync recordings with disk if enabled
|
||||
if self.config.record.sync_on_startup:
|
||||
sync_recordings(limited=False)
|
||||
|
||||
next_sync = get_tomorrow_at_time(3)
|
||||
self.sync_recordings()
|
||||
|
||||
# Expire tmp clips every minute, recordings and clean directories every hour.
|
||||
for counter in itertools.cycle(range(self.config.record.expire_interval)):
|
||||
if self.stop_event.wait(60):
|
||||
logger.info("Exiting recording cleanup...")
|
||||
break
|
||||
|
||||
self.clean_tmp_clips()
|
||||
|
||||
if datetime.datetime.now().astimezone(datetime.timezone.utc) > next_sync:
|
||||
sync_recordings(limited=True)
|
||||
next_sync = get_tomorrow_at_time(3)
|
||||
|
||||
if counter == 0:
|
||||
self.expire_recordings()
|
||||
remove_empty_directories(RECORD_DIR)
|
||||
|
@@ -6,7 +6,6 @@ import os
|
||||
import subprocess as sp
|
||||
import threading
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
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)}"
|
||||
)
|
||||
logger.error(p.stderr)
|
||||
Path(file_name).unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
logger.debug(f"Updating finalized export {file_name}")
|
||||
|
@@ -260,10 +260,8 @@ class RecordingMaintainer(threading.Thread):
|
||||
most_recently_processed_frame_time = (
|
||||
camera_info[-1][0] if len(camera_info) > 0 else 0
|
||||
)
|
||||
retain_cutoff = datetime.datetime.fromtimestamp(
|
||||
most_recently_processed_frame_time - pre_capture
|
||||
).astimezone(datetime.timezone.utc)
|
||||
if end_time.astimezone(datetime.timezone.utc) < retain_cutoff:
|
||||
retain_cutoff = most_recently_processed_frame_time - pre_capture
|
||||
if end_time.timestamp() < retain_cutoff:
|
||||
Path(cache_path).unlink(missing_ok=True)
|
||||
self.end_time_cache.pop(cache_path, None)
|
||||
# else retain days includes this segment
|
||||
@@ -275,11 +273,7 @@ class RecordingMaintainer(threading.Thread):
|
||||
)
|
||||
|
||||
# ensure delayed segment info does not lead to lost segments
|
||||
if datetime.datetime.fromtimestamp(
|
||||
most_recently_processed_frame_time
|
||||
).astimezone(datetime.timezone.utc) >= end_time.astimezone(
|
||||
datetime.timezone.utc
|
||||
):
|
||||
if most_recently_processed_frame_time >= end_time.timestamp():
|
||||
record_mode = self.config.cameras[camera].record.retain.mode
|
||||
return await self.move_segment(
|
||||
camera, start_time, end_time, duration, cache_path, record_mode
|
||||
|
@@ -1,16 +1,7 @@
|
||||
"""Recordings Utilities."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
|
||||
from peewee import DatabaseError, chunked
|
||||
|
||||
from frigate.const import RECORD_DIR
|
||||
from frigate.models import Recordings, RecordingsToDelete
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_empty_directories(directory: str) -> None:
|
||||
# list all directories recursively and sort them by path,
|
||||
@@ -26,110 +17,3 @@ def remove_empty_directories(directory: str) -> None:
|
||||
continue
|
||||
if len(os.listdir(path)) == 0:
|
||||
os.rmdir(path)
|
||||
|
||||
|
||||
def sync_recordings(limited: bool) -> None:
|
||||
"""Check the db for stale recordings entries that don't exist in the filesystem."""
|
||||
|
||||
def delete_db_entries_without_file(files_on_disk: list[str]) -> bool:
|
||||
"""Delete db entries where file was deleted outside of frigate."""
|
||||
|
||||
if limited:
|
||||
recordings = Recordings.select(Recordings.id, Recordings.path).where(
|
||||
Recordings.start_time
|
||||
>= (datetime.datetime.now() - datetime.timedelta(hours=36)).timestamp()
|
||||
)
|
||||
else:
|
||||
# get all recordings in the db
|
||||
recordings = Recordings.select(Recordings.id, Recordings.path)
|
||||
|
||||
# Use pagination to process records in chunks
|
||||
page_size = 1000
|
||||
num_pages = (recordings.count() + page_size - 1) // page_size
|
||||
recordings_to_delete = set()
|
||||
|
||||
for page in range(num_pages):
|
||||
for recording in recordings.paginate(page, page_size):
|
||||
if recording.path not in files_on_disk:
|
||||
recordings_to_delete.add(recording.id)
|
||||
|
||||
# convert back to list of dictionaries for insertion
|
||||
recordings_to_delete = [
|
||||
{"id": recording_id} for recording_id in recordings_to_delete
|
||||
]
|
||||
|
||||
if float(len(recordings_to_delete)) / max(1, recordings.count()) > 0.5:
|
||||
logger.debug(
|
||||
f"Deleting {(float(len(recordings_to_delete)) / recordings.count()):2f}% of recordings DB entries, could be due to configuration error. Aborting..."
|
||||
)
|
||||
return False
|
||||
|
||||
logger.debug(
|
||||
f"Deleting {len(recordings_to_delete)} recording DB entries with missing files"
|
||||
)
|
||||
|
||||
# create a temporary table for deletion
|
||||
RecordingsToDelete.create_table(temporary=True)
|
||||
|
||||
# insert ids to the temporary table
|
||||
max_inserts = 1000
|
||||
for batch in chunked(recordings_to_delete, max_inserts):
|
||||
RecordingsToDelete.insert_many(batch).execute()
|
||||
|
||||
try:
|
||||
# delete records in the main table that exist in the temporary table
|
||||
query = Recordings.delete().where(
|
||||
Recordings.id.in_(RecordingsToDelete.select(RecordingsToDelete.id))
|
||||
)
|
||||
query.execute()
|
||||
except DatabaseError as e:
|
||||
logger.error(f"Database error during recordings db cleanup: {e}")
|
||||
|
||||
return True
|
||||
|
||||
def delete_files_without_db_entry(files_on_disk: list[str]):
|
||||
"""Delete files where file is not inside frigate db."""
|
||||
files_to_delete = []
|
||||
|
||||
for file in files_on_disk:
|
||||
if not Recordings.select().where(Recordings.path == file).exists():
|
||||
files_to_delete.append(file)
|
||||
|
||||
if float(len(files_to_delete)) / max(1, len(files_on_disk)) > 0.5:
|
||||
logger.debug(
|
||||
f"Deleting {(float(len(files_to_delete)) / len(files_on_disk)):2f}% of recordings DB entries, could be due to configuration error. Aborting..."
|
||||
)
|
||||
return
|
||||
|
||||
for file in files_to_delete:
|
||||
os.unlink(file)
|
||||
|
||||
logger.debug("Start sync recordings.")
|
||||
|
||||
if limited:
|
||||
# get recording files from last 36 hours
|
||||
hour_check = (
|
||||
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
||||
- datetime.timedelta(hours=36)
|
||||
).strftime("%Y-%m-%d/%H")
|
||||
files_on_disk = {
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(RECORD_DIR)
|
||||
for file in files
|
||||
if file > hour_check
|
||||
}
|
||||
else:
|
||||
# get all recordings files on disk and put them in a set
|
||||
files_on_disk = {
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(RECORD_DIR)
|
||||
for file in files
|
||||
}
|
||||
|
||||
db_success = delete_db_entries_without_file(files_on_disk)
|
||||
|
||||
# only try to cleanup files if db cleanup was successful
|
||||
if db_success:
|
||||
delete_files_without_db_entry(files_on_disk)
|
||||
|
||||
logger.debug("End sync recordings.")
|
||||
|
@@ -159,13 +159,9 @@ class StorageMaintainer(threading.Thread):
|
||||
|
||||
# Delete recordings not retained indefinitely
|
||||
if not keep:
|
||||
try:
|
||||
Path(recording.path).unlink(missing_ok=False)
|
||||
deleted_recordings.add(recording.id)
|
||||
deleted_segments_size += recording.segment_size
|
||||
except FileNotFoundError:
|
||||
# this file was not found so we must assume no space was cleaned up
|
||||
pass
|
||||
deleted_segments_size += recording.segment_size
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
deleted_recordings.add(recording.id)
|
||||
|
||||
# check if need to delete retained segments
|
||||
if deleted_segments_size < hourly_bandwidth:
|
||||
@@ -187,15 +183,9 @@ class StorageMaintainer(threading.Thread):
|
||||
if deleted_segments_size > hourly_bandwidth:
|
||||
break
|
||||
|
||||
try:
|
||||
Path(recording.path).unlink(missing_ok=False)
|
||||
deleted_segments_size += recording.segment_size
|
||||
deleted_recordings.add(recording.id)
|
||||
except FileNotFoundError:
|
||||
# this file was not found so we must assume no space was cleaned up
|
||||
pass
|
||||
else:
|
||||
logger.info(f"Cleaned up {deleted_segments_size} MB of recordings")
|
||||
deleted_segments_size += recording.segment_size
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
deleted_recordings.add(recording.id)
|
||||
|
||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||
# delete up to 100,000 at a time
|
||||
|
@@ -1651,11 +1651,11 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
runtime_config = frigate_config.runtime_config()
|
||||
assert runtime_config.cameras["back"].onvif.autotracking.movement_weights == [
|
||||
"0.0",
|
||||
"1.0",
|
||||
"1.23",
|
||||
"2.34",
|
||||
"0.5",
|
||||
0,
|
||||
1,
|
||||
1.23,
|
||||
2.34,
|
||||
0.50,
|
||||
]
|
||||
|
||||
def test_fails_invalid_movement_weights(self):
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -27,7 +26,6 @@ class TestHttp(unittest.TestCase):
|
||||
self.db = SqliteQueueDatabase(TEST_DB)
|
||||
models = [Event, Recordings]
|
||||
self.db.bind(models)
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
|
||||
self.minimal_config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
@@ -96,7 +94,6 @@ class TestHttp(unittest.TestCase):
|
||||
rec_bd_id = "1234568.backdoor"
|
||||
_insert_mock_recording(
|
||||
rec_fd_id,
|
||||
os.path.join(self.test_dir, f"{rec_fd_id}.tmp"),
|
||||
time_keep,
|
||||
time_keep + 10,
|
||||
camera="front_door",
|
||||
@@ -105,7 +102,6 @@ class TestHttp(unittest.TestCase):
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_bd_id,
|
||||
os.path.join(self.test_dir, f"{rec_bd_id}.tmp"),
|
||||
time_keep + 10,
|
||||
time_keep + 20,
|
||||
camera="back_door",
|
||||
@@ -127,7 +123,6 @@ class TestHttp(unittest.TestCase):
|
||||
rec_fd_id = "1234567.frontdoor"
|
||||
_insert_mock_recording(
|
||||
rec_fd_id,
|
||||
os.path.join(self.test_dir, f"{rec_fd_id}.tmp"),
|
||||
time_keep,
|
||||
time_keep + 10,
|
||||
camera="front_door",
|
||||
@@ -146,33 +141,13 @@ class TestHttp(unittest.TestCase):
|
||||
|
||||
id = "123456.keep"
|
||||
time_keep = datetime.datetime.now().timestamp()
|
||||
_insert_mock_event(
|
||||
id,
|
||||
time_keep,
|
||||
time_keep + 30,
|
||||
True,
|
||||
)
|
||||
_insert_mock_event(id, time_keep, time_keep + 30, True)
|
||||
rec_k_id = "1234567.keep"
|
||||
rec_k2_id = "1234568.keep"
|
||||
rec_k3_id = "1234569.keep"
|
||||
_insert_mock_recording(
|
||||
rec_k_id,
|
||||
os.path.join(self.test_dir, f"{rec_k_id}.tmp"),
|
||||
time_keep,
|
||||
time_keep + 10,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_k2_id,
|
||||
os.path.join(self.test_dir, f"{rec_k2_id}.tmp"),
|
||||
time_keep + 10,
|
||||
time_keep + 20,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_k3_id,
|
||||
os.path.join(self.test_dir, f"{rec_k3_id}.tmp"),
|
||||
time_keep + 20,
|
||||
time_keep + 30,
|
||||
)
|
||||
_insert_mock_recording(rec_k_id, time_keep, time_keep + 10)
|
||||
_insert_mock_recording(rec_k2_id, time_keep + 10, time_keep + 20)
|
||||
_insert_mock_recording(rec_k3_id, time_keep + 20, time_keep + 30)
|
||||
|
||||
id2 = "7890.delete"
|
||||
time_delete = datetime.datetime.now().timestamp() - 360
|
||||
@@ -180,24 +155,9 @@ class TestHttp(unittest.TestCase):
|
||||
rec_d_id = "78901.delete"
|
||||
rec_d2_id = "78902.delete"
|
||||
rec_d3_id = "78903.delete"
|
||||
_insert_mock_recording(
|
||||
rec_d_id,
|
||||
os.path.join(self.test_dir, f"{rec_d_id}.tmp"),
|
||||
time_delete,
|
||||
time_delete + 10,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_d2_id,
|
||||
os.path.join(self.test_dir, f"{rec_d2_id}.tmp"),
|
||||
time_delete + 10,
|
||||
time_delete + 20,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_d3_id,
|
||||
os.path.join(self.test_dir, f"{rec_d3_id}.tmp"),
|
||||
time_delete + 20,
|
||||
time_delete + 30,
|
||||
)
|
||||
_insert_mock_recording(rec_d_id, time_delete, time_delete + 10)
|
||||
_insert_mock_recording(rec_d2_id, time_delete + 10, time_delete + 20)
|
||||
_insert_mock_recording(rec_d3_id, time_delete + 20, time_delete + 30)
|
||||
|
||||
storage.calculate_camera_bandwidth()
|
||||
storage.reduce_storage_consumption()
|
||||
@@ -216,42 +176,18 @@ class TestHttp(unittest.TestCase):
|
||||
|
||||
id = "123456.keep"
|
||||
time_keep = datetime.datetime.now().timestamp()
|
||||
_insert_mock_event(
|
||||
id,
|
||||
time_keep,
|
||||
time_keep + 30,
|
||||
True,
|
||||
)
|
||||
_insert_mock_event(id, time_keep, time_keep + 30, True)
|
||||
rec_k_id = "1234567.keep"
|
||||
rec_k2_id = "1234568.keep"
|
||||
rec_k3_id = "1234569.keep"
|
||||
_insert_mock_recording(
|
||||
rec_k_id,
|
||||
os.path.join(self.test_dir, f"{rec_k_id}.tmp"),
|
||||
time_keep,
|
||||
time_keep + 10,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_k2_id,
|
||||
os.path.join(self.test_dir, f"{rec_k2_id}.tmp"),
|
||||
time_keep + 10,
|
||||
time_keep + 20,
|
||||
)
|
||||
_insert_mock_recording(
|
||||
rec_k3_id,
|
||||
os.path.join(self.test_dir, f"{rec_k3_id}.tmp"),
|
||||
time_keep + 20,
|
||||
time_keep + 30,
|
||||
)
|
||||
_insert_mock_recording(rec_k_id, time_keep, time_keep + 10)
|
||||
_insert_mock_recording(rec_k2_id, time_keep + 10, time_keep + 20)
|
||||
_insert_mock_recording(rec_k3_id, time_keep + 20, time_keep + 30)
|
||||
|
||||
time_delete = datetime.datetime.now().timestamp() - 7200
|
||||
for i in range(0, 59):
|
||||
id = f"{123456 + i}.delete"
|
||||
_insert_mock_recording(
|
||||
id,
|
||||
os.path.join(self.test_dir, f"{id}.tmp"),
|
||||
time_delete,
|
||||
time_delete + 600,
|
||||
f"{123456 + i}.delete", time_delete, time_delete + 600
|
||||
)
|
||||
|
||||
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(
|
||||
id: str,
|
||||
file: str,
|
||||
start: int,
|
||||
end: int,
|
||||
camera="front_door",
|
||||
seg_size=8,
|
||||
seg_dur=10,
|
||||
id: str, start: int, end: int, camera="front_door", seg_size=8, seg_dur=10
|
||||
) -> Event:
|
||||
"""Inserts a basic recording model with a given id."""
|
||||
# we must open the file so storage maintainer will delete it
|
||||
with open(file, "w"):
|
||||
pass
|
||||
|
||||
return Recordings.insert(
|
||||
id=id,
|
||||
camera=camera,
|
||||
path=file,
|
||||
path=f"/recordings/{id}",
|
||||
start_time=start,
|
||||
end_time=end,
|
||||
duration=seg_dur,
|
||||
|
@@ -68,6 +68,7 @@ class NorfairTracker(ObjectTracker):
|
||||
self.untracked_object_boxes: list[list[int]] = []
|
||||
self.disappeared = {}
|
||||
self.positions = {}
|
||||
self.max_disappeared = config.detect.max_disappeared
|
||||
self.camera_config = config
|
||||
self.detect_config = config.detect
|
||||
self.ptz_metrics = ptz_metrics
|
||||
@@ -80,8 +81,8 @@ class NorfairTracker(ObjectTracker):
|
||||
self.tracker = Tracker(
|
||||
distance_function=frigate_distance,
|
||||
distance_threshold=2.5,
|
||||
initialization_delay=self.detect_config.min_initialized,
|
||||
hit_counter_max=self.detect_config.max_disappeared,
|
||||
initialization_delay=self.detect_config.fps / 2,
|
||||
hit_counter_max=self.max_disappeared,
|
||||
)
|
||||
if self.ptz_autotracker_enabled.value:
|
||||
self.ptz_motion_estimator = PtzMotionEstimator(
|
||||
|
@@ -31,8 +31,7 @@ class CameraMetricsTypes(TypedDict):
|
||||
|
||||
class PTZMetricsTypes(TypedDict):
|
||||
ptz_autotracker_enabled: Synchronized
|
||||
ptz_tracking_active: Event
|
||||
ptz_motor_stopped: Event
|
||||
ptz_stopped: Event
|
||||
ptz_reset: Event
|
||||
ptz_start_time: Synchronized
|
||||
ptz_stop_time: Synchronized
|
||||
|
@@ -114,8 +114,10 @@ def load_config_with_no_duplicates(raw_config) -> dict:
|
||||
|
||||
def clean_camera_user_pass(line: str) -> str:
|
||||
"""Removes user and password from line."""
|
||||
rtsp_cleaned = re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line)
|
||||
return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", rtsp_cleaned)
|
||||
if "rtsp://" in line:
|
||||
return re.sub(REGEX_RTSP_CAMERA_USER_PASS, "://*:*@", line)
|
||||
else:
|
||||
return re.sub(REGEX_HTTP_CAMERA_USER_PASS, "user=*&password=*", line)
|
||||
|
||||
|
||||
def escape_special_characters(path: str) -> str:
|
||||
@@ -263,9 +265,8 @@ def find_by_key(dictionary, target_key):
|
||||
return None
|
||||
|
||||
|
||||
def get_tomorrow_at_time(hour: int) -> datetime.datetime:
|
||||
"""Returns the datetime of the following day at 2am."""
|
||||
def get_tomorrow_at_2() -> datetime.datetime:
|
||||
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
|
||||
)
|
||||
|
@@ -174,9 +174,9 @@ def get_region_from_grid(
|
||||
|
||||
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"]:
|
||||
return box
|
||||
return calculate_region(frame_shape, box[0], box[1], box[2], box[3], min_region)
|
||||
|
||||
# convert the calculated region size to relative
|
||||
calc_size = (box[2] - box[0]) / frame_shape[1]
|
||||
|
@@ -26,7 +26,7 @@ from frigate.ptz.autotrack import ptz_moving_at_frame_time
|
||||
from frigate.track import ObjectTracker
|
||||
from frigate.track.norfair_tracker import NorfairTracker
|
||||
from frigate.types import PTZMetricsTypes
|
||||
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_time
|
||||
from frigate.util.builtin import EventsPerSecond, get_tomorrow_at_2
|
||||
from frigate.util.image import (
|
||||
FrameManager,
|
||||
SharedMemoryFrameManager,
|
||||
@@ -233,15 +233,14 @@ class CameraWatchdog(threading.Thread):
|
||||
poll = p["process"].poll()
|
||||
|
||||
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(
|
||||
"latest_segment_time",
|
||||
datetime.datetime.now().astimezone(datetime.timezone.utc),
|
||||
"latest_segment_time", datetime.datetime.now().timestamp()
|
||||
)
|
||||
)
|
||||
|
||||
if datetime.datetime.now().astimezone(datetime.timezone.utc) > (
|
||||
latest_segment_time + datetime.timedelta(seconds=120)
|
||||
if datetime.datetime.now().timestamp() > (
|
||||
latest_segment_time + 120
|
||||
):
|
||||
self.logger.error(
|
||||
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()
|
||||
|
||||
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."""
|
||||
cache_files = sorted(
|
||||
[
|
||||
@@ -300,15 +299,13 @@ class CameraWatchdog(threading.Thread):
|
||||
and not d.startswith("clip_")
|
||||
]
|
||||
)
|
||||
newest_segment_timestamp = latest_segment
|
||||
newest_segment_timestamp = latest_timestamp
|
||||
|
||||
for file in cache_files:
|
||||
if self.camera_name in file:
|
||||
basename = os.path.splitext(file)[0]
|
||||
_, date = basename.rsplit("-", maxsplit=1)
|
||||
ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").astimezone(
|
||||
datetime.timezone.utc
|
||||
)
|
||||
ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").timestamp()
|
||||
if ts > newest_segment_timestamp:
|
||||
newest_segment_timestamp = ts
|
||||
|
||||
@@ -528,7 +525,7 @@ def process_frames(
|
||||
fps = process_info["process_fps"]
|
||||
detection_fps = process_info["detection_fps"]
|
||||
current_frame_time = process_info["detection_frame"]
|
||||
next_region_update = get_tomorrow_at_time(2)
|
||||
next_region_update = get_tomorrow_at_2()
|
||||
|
||||
fps_tracker = EventsPerSecond()
|
||||
fps_tracker.start()
|
||||
@@ -550,7 +547,7 @@ def process_frames(
|
||||
except queue.Empty:
|
||||
logger.error(f"Unable to get updated region grid for {camera_name}")
|
||||
|
||||
next_region_update = get_tomorrow_at_time(2)
|
||||
next_region_update = get_tomorrow_at_2()
|
||||
|
||||
try:
|
||||
if exit_on_empty:
|
||||
|
301
web/package-lock.json
generated
301
web/package-lock.json
generated
@@ -1664,16 +1664,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz",
|
||||
"integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
|
||||
"integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/type-utils": "6.9.1",
|
||||
"@typescript-eslint/utils": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/type-utils": "6.8.0",
|
||||
"@typescript-eslint/utils": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -1870,15 +1870,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
|
||||
"integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
|
||||
"integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1898,13 +1898,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz",
|
||||
"integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
|
||||
"integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1"
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1915,13 +1915,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz",
|
||||
"integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
|
||||
"integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/utils": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"@typescript-eslint/utils": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1942,9 +1942,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz",
|
||||
"integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
|
||||
"integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1955,13 +1955,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz",
|
||||
"integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
|
||||
"integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1997,17 +1997,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz",
|
||||
"integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
|
||||
"integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2037,12 +2037,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz",
|
||||
"integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
|
||||
"integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2060,17 +2060,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@videojs/http-streaming": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.7.0.tgz",
|
||||
"integrity": "sha512-5uLFKBL8CvD56dxxJyuxqB5CY0tdoa4SE9KbXakeiAy6iFBUEPvTr2YGLKEWvQ8Lojs1wl+FQndLdv+GO7t9Fw==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
|
||||
"integrity": "sha512-dty8lsZk9QPc0i4It79tjWsmPiaC3FpgARFM0vJGko4k3yKNZIYkAk8kjiDRfkAQH/HZ3rYi5dDTriFNzwSsIg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "4.0.0",
|
||||
"aes-decrypter": "4.0.1",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "^7.1.0",
|
||||
"mpd-parser": "^1.2.2",
|
||||
"mux.js": "7.0.1",
|
||||
"mpd-parser": "^1.1.1",
|
||||
"mux.js": "7.0.0",
|
||||
"video.js": "^7 || ^8"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2105,6 +2105,22 @@
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/http-streaming/node_modules/mux.js": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.0.tgz",
|
||||
"integrity": "sha512-DeZmr+3NDrO02k4SREtl4VB5GyGPCz2fzMjDxBIlamkxffSTLge97rtNMoonnmFHTp96QggDucUtKv3fmyObrA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"muxjs-transmux": "bin/transmux.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/vhs-utils": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz",
|
||||
@@ -2653,9 +2669,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
|
||||
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
||||
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -4062,9 +4078,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jest": {
|
||||
"version": "27.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz",
|
||||
"integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==",
|
||||
"version": "27.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
|
||||
"integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^5.10.0"
|
||||
@@ -6110,9 +6126,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
||||
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
|
||||
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -6913,9 +6929,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mux.js": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.1.tgz",
|
||||
"integrity": "sha512-Omz79uHqYpMP1V80JlvEdCiOW1hiw4mBvDh9gaZEpxvB+7WYb2soZSzfuSRrK2Kh9Pm6eugQNrIpY/Bnyhk4hw==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz",
|
||||
"integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
@@ -8650,9 +8666,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -8660,10 +8676,10 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.19.1",
|
||||
"jiti": "^1.18.2",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -9153,12 +9169,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/video.js": {
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.6.1.tgz",
|
||||
"integrity": "sha512-CNYVJ5WWIZ7bOhbkkfcKqLGoc6WsE3Ft2RfS1lXdQTWk8UiSsPW2Ssk2JzPCA8qnIlUG9os/faCFsYWjyu4JcA==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.5.2.tgz",
|
||||
"integrity": "sha512-6/uNXQV3xSaKLpaPf/bVvr7omd+82sKUp0RMBgIt4PxHIe28GtX+O+GcNfI2fuwBvcDRDqk5Ei5AG9bJJOpulA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "3.7.0",
|
||||
"@videojs/http-streaming": "3.5.3",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@videojs/xhr": "2.6.0",
|
||||
"aes-decrypter": "^4.0.1",
|
||||
@@ -9166,7 +9182,7 @@
|
||||
"keycode": "2.2.0",
|
||||
"m3u8-parser": "^6.0.0",
|
||||
"mpd-parser": "^1.0.1",
|
||||
"mux.js": "^7.0.1",
|
||||
"mux.js": "^6.2.0",
|
||||
"safe-json-parse": "4.0.0",
|
||||
"videojs-contrib-quality-levels": "4.0.0",
|
||||
"videojs-font": "4.1.0",
|
||||
@@ -10821,16 +10837,16 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz",
|
||||
"integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz",
|
||||
"integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/type-utils": "6.9.1",
|
||||
"@typescript-eslint/utils": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/type-utils": "6.8.0",
|
||||
"@typescript-eslint/utils": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -10944,54 +10960,54 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz",
|
||||
"integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz",
|
||||
"integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz",
|
||||
"integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz",
|
||||
"integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1"
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz",
|
||||
"integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz",
|
||||
"integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/utils": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"@typescript-eslint/utils": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz",
|
||||
"integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz",
|
||||
"integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz",
|
||||
"integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz",
|
||||
"integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/visitor-keys": "6.9.1",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/visitor-keys": "6.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -11011,17 +11027,17 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz",
|
||||
"integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz",
|
||||
"integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.9.1",
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/typescript-estree": "6.9.1",
|
||||
"@typescript-eslint/scope-manager": "6.8.0",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"@typescript-eslint/typescript-estree": "6.8.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -11037,12 +11053,12 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz",
|
||||
"integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==",
|
||||
"version": "6.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz",
|
||||
"integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "6.9.1",
|
||||
"@typescript-eslint/types": "6.8.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
}
|
||||
},
|
||||
@@ -11053,17 +11069,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"@videojs/http-streaming": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.7.0.tgz",
|
||||
"integrity": "sha512-5uLFKBL8CvD56dxxJyuxqB5CY0tdoa4SE9KbXakeiAy6iFBUEPvTr2YGLKEWvQ8Lojs1wl+FQndLdv+GO7t9Fw==",
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.5.3.tgz",
|
||||
"integrity": "sha512-dty8lsZk9QPc0i4It79tjWsmPiaC3FpgARFM0vJGko4k3yKNZIYkAk8kjiDRfkAQH/HZ3rYi5dDTriFNzwSsIg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "4.0.0",
|
||||
"aes-decrypter": "4.0.1",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "^7.1.0",
|
||||
"mpd-parser": "^1.2.2",
|
||||
"mux.js": "7.0.1",
|
||||
"mpd-parser": "^1.1.1",
|
||||
"mux.js": "7.0.0",
|
||||
"video.js": "^7 || ^8"
|
||||
},
|
||||
"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
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
|
||||
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
||||
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -12573,9 +12598,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-jest": {
|
||||
"version": "27.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz",
|
||||
"integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==",
|
||||
"version": "27.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.3.tgz",
|
||||
"integrity": "sha512-7S6SmmsHsgIm06BAGCAxL+ABd9/IB3MWkz2pudj6Qqor2y1qQpWPfuFU4SG9pWj4xDjF0e+D7Llh5useuSzAZw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/utils": "^5.10.0"
|
||||
@@ -13937,9 +13962,9 @@
|
||||
}
|
||||
},
|
||||
"jiti": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
||||
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
|
||||
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
|
||||
"dev": true
|
||||
},
|
||||
"js-levenshtein": {
|
||||
@@ -14540,9 +14565,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"mux.js": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.1.tgz",
|
||||
"integrity": "sha512-Omz79uHqYpMP1V80JlvEdCiOW1hiw4mBvDh9gaZEpxvB+7WYb2soZSzfuSRrK2Kh9Pm6eugQNrIpY/Bnyhk4hw==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz",
|
||||
"integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
@@ -15789,9 +15814,9 @@
|
||||
}
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@@ -15799,10 +15824,10 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.19.1",
|
||||
"jiti": "^1.18.2",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -16178,12 +16203,12 @@
|
||||
}
|
||||
},
|
||||
"video.js": {
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.6.1.tgz",
|
||||
"integrity": "sha512-CNYVJ5WWIZ7bOhbkkfcKqLGoc6WsE3Ft2RfS1lXdQTWk8UiSsPW2Ssk2JzPCA8qnIlUG9os/faCFsYWjyu4JcA==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.5.2.tgz",
|
||||
"integrity": "sha512-6/uNXQV3xSaKLpaPf/bVvr7omd+82sKUp0RMBgIt4PxHIe28GtX+O+GcNfI2fuwBvcDRDqk5Ei5AG9bJJOpulA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "3.7.0",
|
||||
"@videojs/http-streaming": "3.5.3",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@videojs/xhr": "2.6.0",
|
||||
"aes-decrypter": "^4.0.1",
|
||||
@@ -16191,7 +16216,7 @@
|
||||
"keycode": "2.2.0",
|
||||
"m3u8-parser": "^6.0.0",
|
||||
"mpd-parser": "^1.0.1",
|
||||
"mux.js": "^7.0.1",
|
||||
"mux.js": "^6.2.0",
|
||||
"safe-json-parse": "4.0.0",
|
||||
"videojs-contrib-quality-levels": "4.0.0",
|
||||
"videojs-font": "4.1.0",
|
||||
|
@@ -7,7 +7,6 @@ import axios from 'axios';
|
||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||
axios.defaults.headers.common = {
|
||||
'X-CSRF-TOKEN': 1,
|
||||
'X-CACHE-BYPASS': 1,
|
||||
};
|
||||
|
||||
export function ApiProvider({ children, options }) {
|
||||
|
@@ -67,7 +67,6 @@ export default function Button({
|
||||
disabled = false,
|
||||
ariaCapitalize = false,
|
||||
href,
|
||||
target,
|
||||
type = 'contained',
|
||||
...attrs
|
||||
}) {
|
||||
@@ -102,7 +101,6 @@ export default function Button({
|
||||
tabindex="0"
|
||||
className={classes}
|
||||
href={href}
|
||||
target={target}
|
||||
ref={ref}
|
||||
onmouseenter={handleMousenter}
|
||||
onmouseleave={handleMouseleave}
|
||||
|
@@ -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);
|
@@ -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);
|
@@ -11,7 +11,6 @@ import axios from 'axios';
|
||||
import { useState, useRef, useCallback, useMemo } from 'preact/hooks';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import { StarRecording } from '../icons/StarRecording';
|
||||
import { Submitted } from '../icons/Submitted';
|
||||
import { Snapshot } from '../icons/Snapshot';
|
||||
import { UploadPlus } from '../icons/UploadPlus';
|
||||
import { Clip } from '../icons/Clip';
|
||||
@@ -64,7 +63,6 @@ export default function Events({ path, ...props }) {
|
||||
time_range: '00:00,24:00',
|
||||
timezone,
|
||||
favorites: props.favorites ?? 0,
|
||||
is_submitted: props.is_submitted ?? -1,
|
||||
event: props.event,
|
||||
});
|
||||
const [state, setState] = useState({
|
||||
@@ -283,16 +281,6 @@ export default function Events({ path, ...props }) {
|
||||
[path, searchParams, setSearchParams]
|
||||
);
|
||||
|
||||
const onClickFilterSubmitted = useCallback(
|
||||
() => {
|
||||
if( ++searchParams.is_submitted > 1 ) {
|
||||
searchParams.is_submitted = -1;
|
||||
}
|
||||
onFilter('is_submitted', searchParams.is_submitted);
|
||||
},
|
||||
[searchParams, onFilter]
|
||||
);
|
||||
|
||||
const isDone = (eventPages?.[eventPages.length - 1]?.length ?? 0) < API_LIMIT;
|
||||
|
||||
// hooks for infinite scroll
|
||||
@@ -406,22 +394,11 @@ export default function Events({ path, ...props }) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex">
|
||||
{config.plus.enabled && (
|
||||
<Submitted
|
||||
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
|
||||
onClick={() => onClickFilterSubmitted()}
|
||||
inner_fill={searchParams.is_submitted == 1 ? 'currentColor' : 'gray'}
|
||||
outer_stroke={searchParams.is_submitted >= 0 ? 'currentColor' : 'gray'}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StarRecording
|
||||
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
|
||||
onClick={() => onFilter('favorites', searchParams.favorites ? 0 : 1)}
|
||||
fill={searchParams.favorites == 1 ? 'currentColor' : 'none'}
|
||||
/>
|
||||
</div>
|
||||
<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 ref={datePicker} className="ml-right">
|
||||
<CalendarIcon
|
||||
|
@@ -12,7 +12,6 @@ import Dialog from '../components/Dialog';
|
||||
import TimeAgo from '../components/TimeAgo';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { About } from '../icons/About';
|
||||
import { WebUI } from '../icons/WebUI';
|
||||
|
||||
const emptyObject = Object.freeze({});
|
||||
|
||||
@@ -348,17 +347,7 @@ export default function System() {
|
||||
>
|
||||
<div className="capitalize text-lg flex justify-between p-4">
|
||||
<Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
|
||||
<div className="flex">
|
||||
{config.cameras[camera]['webui_url'] && (
|
||||
<Button
|
||||
href={config.cameras[camera]['webui_url']}
|
||||
target="_blank"
|
||||
>
|
||||
Web UI<WebUI className="ml-1 h-4 w-4" fill="white" stroke="white" />
|
||||
</Button>
|
||||
)}
|
||||
<Button className="ml-2" onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
|
||||
</div>
|
||||
<Button onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Table className="w-full">
|
||||
|
Reference in New Issue
Block a user