mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 09:15:22 +03:00
Merge remote-tracking branch 'origin/release-0.11.0' into gstreamer
This commit is contained in:
commit
1240a073c9
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build and run tests
|
||||
run: make run_tests PLATFORM="linux/arm64/v8" ARCH="aarch64" FFMPEG_ARCH="arm64"
|
||||
run: make run_tests PLATFORM="linux/arm64/v8" ARCH="aarch64"
|
||||
|
||||
docker_tests_on_amd64:
|
||||
runs-on: ubuntu-latest
|
||||
@ -67,4 +67,4 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Build and run tests
|
||||
run: make run_tests PLATFORM="linux/amd64" ARCH="amd64" FFMPEG_ARCH="amd64"
|
||||
run: make run_tests PLATFORM="linux/amd64" ARCH="amd64"
|
||||
|
||||
4
Makefile
4
Makefile
@ -3,7 +3,7 @@ default_target: amd64_frigate
|
||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||
|
||||
version:
|
||||
echo "VERSION='0.10.0-$(COMMIT_HASH)'" > frigate/version.py
|
||||
echo "VERSION='0.11.0-$(COMMIT_HASH)'" > frigate/version.py
|
||||
|
||||
web:
|
||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||
@ -91,7 +91,7 @@ run_tests:
|
||||
@sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test
|
||||
@echo "" >> docker/Dockerfile.test
|
||||
@echo "RUN python3 -m unittest" >> docker/Dockerfile.test
|
||||
@docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg FFMPEG_ARCH=$(FFMPEG_ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test .
|
||||
@docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test .
|
||||
@rm docker/Dockerfile.test
|
||||
|
||||
.PHONY: web run_tests
|
||||
|
||||
127
docker/Dockerfile
Normal file
127
docker/Dockerfile
Normal file
@ -0,0 +1,127 @@
|
||||
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
|
||||
|
||||
FROM node:14 as web
|
||||
|
||||
WORKDIR /opt/frigate
|
||||
|
||||
COPY web/ .
|
||||
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM debian:11 as wheels
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Use a separate container to build wheels to prevent build dependencies in final image
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
apt-transport-https \
|
||||
gnupg \
|
||||
wget \
|
||||
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
|
||||
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
|
||||
&& apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
wget \
|
||||
# opencv dependencies
|
||||
build-essential cmake git pkg-config libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||
# scipy dependencies
|
||||
gcc gfortran libopenblas-dev liblapack-dev
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py "pip"
|
||||
|
||||
RUN pip3 install scikit-build
|
||||
|
||||
# TODO: lock with requirements.txt
|
||||
RUN pip3 wheel --wheel-dir=/wheels \
|
||||
opencv-python-headless \
|
||||
numpy \
|
||||
imutils \
|
||||
scipy \
|
||||
psutil \
|
||||
Flask \
|
||||
paho-mqtt \
|
||||
PyYAML \
|
||||
matplotlib \
|
||||
click \
|
||||
setproctitle \
|
||||
peewee \
|
||||
peewee_migrate \
|
||||
pydantic \
|
||||
zeroconf \
|
||||
ws4py
|
||||
|
||||
# Frigate Container
|
||||
FROM debian:11-slim
|
||||
ARG TARGETARCH
|
||||
ARG S6_OVERLAY_VERSION=3.0.0.2
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV FLASK_ENV=development
|
||||
|
||||
COPY --from=wheels /wheels /wheels
|
||||
|
||||
# Install ffmpeg
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
apt-transport-https \
|
||||
gnupg \
|
||||
wget \
|
||||
unzip tzdata libxml2 xz-utils \
|
||||
python3-pip \
|
||||
# add raspberry pi repo
|
||||
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \
|
||||
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \
|
||||
# add coral repo
|
||||
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
|
||||
&& apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
ffmpeg \
|
||||
# coral drivers
|
||||
libedgetpu1-max python3-tflite-runtime python3-pycoral \
|
||||
&& pip3 install -U /wheels/*.whl \
|
||||
&& rm -rf /var/lib/apt/lists/* /wheels \
|
||||
&& (apt-get autoremove -y; apt-get autoclean -y)
|
||||
|
||||
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
|
||||
|
||||
# get model and labels
|
||||
COPY labelmap.txt /labelmap.txt
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
ADD migrations migrations/
|
||||
|
||||
COPY --from=web /opt/frigate/build web/
|
||||
|
||||
COPY docker/rootfs/ /
|
||||
|
||||
# s6-overlay
|
||||
RUN S6_ARCH="${TARGETARCH}" \
|
||||
&& if [ "${TARGETARCH}" = "amd64" ]; then S6_ARCH="amd64"; fi \
|
||||
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
|
||||
&& wget -O /tmp/s6-overlay-installer "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-${S6_ARCH}-installer" \
|
||||
&& chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
|
||||
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch-${S6_OVERLAY_VERSION}.tar.xz" \
|
||||
# | tar -C / -Jxpf - \
|
||||
# && wget -O - "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}-${S6_OVERLAY_VERSION}.tar.xz" \
|
||||
# | tar -C / -Jxpf -
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 1935
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
CMD ["python3", "-u", "-m", "frigate"]
|
||||
@ -43,6 +43,11 @@ If you are storing your database on a network share (SMB, NFS, etc), you may get
|
||||
|
||||
This may need to be in a custom location if network storage is used for the media folder.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /path/to/frigate.db
|
||||
```
|
||||
|
||||
### `model`
|
||||
|
||||
If using a custom model, the width and height will need to be specified.
|
||||
|
||||
@ -19,6 +19,34 @@ output_args:
|
||||
rtmp: -c:v libx264 -an -f flv
|
||||
```
|
||||
|
||||
### JPEG Stream Cameras
|
||||
|
||||
Cameras using a live changing jpeg image will need input parameters as below
|
||||
|
||||
```yaml
|
||||
input_args:
|
||||
- -r
|
||||
- 5 # << enter FPS here
|
||||
- -stream_loop
|
||||
- -1
|
||||
- -f
|
||||
- image2
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- 1
|
||||
```
|
||||
|
||||
Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras)
|
||||
|
||||
### RTMP Cameras
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
@ -238,6 +238,11 @@ motion:
|
||||
# NOTE: Can be overridden at the camera level
|
||||
record:
|
||||
# Optional: Enable recording (default: shown below)
|
||||
# WARNING: Frigate does not currently support limiting recordings based
|
||||
# on available disk space automatically. If using recordings,
|
||||
# you must specify retention settings for a number of days that
|
||||
# will fit within the available disk space of your drive or Frigate
|
||||
# will crash.
|
||||
enabled: False
|
||||
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
|
||||
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
|
||||
|
||||
@ -5,7 +5,11 @@ title: Objects
|
||||
|
||||
import labels from "../../../labelmap.txt";
|
||||
|
||||
By default, Frigate includes the following object models from the Google Coral test data. Note that `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
Frigate includes the object models listed below from the Google Coral test data.
|
||||
|
||||
Please note:
|
||||
- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
- `person` is the only tracked object by default. See the [full configuration reference](https://docs.frigate.video/configuration/index#full-configuration-reference) for an example of expanding the list of tracked objects.
|
||||
|
||||
<ul>
|
||||
{labels.split("\n").map((label) => (
|
||||
|
||||
@ -5,4 +5,4 @@ title: RTMP
|
||||
|
||||
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||
|
||||
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization.
|
||||
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](../faqs#audio-in-recordings).
|
||||
|
||||
@ -90,6 +90,20 @@ VSCode will start the docker compose file for you and open a terminal window con
|
||||
|
||||
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
|
||||
|
||||
### Testing
|
||||
|
||||
#### FFMPEG Hardware Acceleration
|
||||
|
||||
The following commands are used inside the container to ensure hardware acceleration is working properly.
|
||||
|
||||
**Raspberry Pi (64bit)**
|
||||
|
||||
This should show <50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`.
|
||||
|
||||
```shell
|
||||
ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
## Web Interface
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@ -11,9 +11,24 @@ This error message is due to a shm-size that is too small. Try updating your shm
|
||||
|
||||
A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
|
||||
|
||||
### How can I get sound or audio in my recordings?
|
||||
### How can I get sound or audio in my recordings? {#audio-in-recordings}
|
||||
|
||||
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
|
||||
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/configuration/index/#full-configuration-reference).
|
||||
|
||||
:::tip
|
||||
|
||||
When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example:
|
||||
|
||||
```diff title="frigate.yml"
|
||||
ffmpeg:
|
||||
output_args:
|
||||
- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac
|
||||
```
|
||||
|
||||
This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`.
|
||||
|
||||
:::
|
||||
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
|
||||
|
||||
@ -21,6 +21,12 @@ Windows is not officially supported, but some users have had success getting it
|
||||
|
||||
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
|
||||
|
||||
:::caution
|
||||
|
||||
Note that Frigate does not currently support limiting recordings based on available disk space automatically. If using recordings, you must specify retention settings for a number of days that will fit within the available disk space of your drive or Frigate will crash.
|
||||
|
||||
:::
|
||||
|
||||
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
|
||||
@ -118,6 +124,7 @@ services:
|
||||
shm_size: "64mb" # update for your cameras based on calculation above
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
|
||||
- /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
|
||||
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
@ -188,6 +188,14 @@ Returns data for a single event.
|
||||
|
||||
Permanently deletes the event along with any clips/snapshots.
|
||||
|
||||
### `POST /api/events/<id>/retain`
|
||||
|
||||
Sets retain to true for the event id.
|
||||
|
||||
### `DELETE /api/events/<id>/retain`
|
||||
|
||||
Sets retain to false for the event id (event may be deleted quickly after removing).
|
||||
|
||||
### `GET /api/events/<id>/thumbnail.jpg`
|
||||
|
||||
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
|
||||
|
||||
@ -7,6 +7,7 @@ import sys
|
||||
import threading
|
||||
from logging.handlers import QueueHandler
|
||||
from typing import Dict, List
|
||||
|
||||
import traceback
|
||||
import yaml
|
||||
from peewee_migrate import Router
|
||||
|
||||
@ -147,6 +147,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the media from disk
|
||||
for event in expired_events:
|
||||
@ -166,6 +167,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
@ -192,6 +194,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
@ -210,6 +213,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ from functools import reduce
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
from flask.helpers import send_file
|
||||
|
||||
import numpy as np
|
||||
from flask import (
|
||||
@ -26,10 +25,10 @@ from flask import (
|
||||
request,
|
||||
)
|
||||
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.const import CLIPS_DIR, RECORD_DIR
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.stats import stats_snapshot
|
||||
from frigate.util import calculate_region
|
||||
@ -120,6 +119,40 @@ def event(id):
|
||||
return "Event not found", 404
|
||||
|
||||
|
||||
@bp.route("/events/<id>/retain", methods=("POST",))
|
||||
def set_retain(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
||||
)
|
||||
|
||||
event.retain_indefinitely = True
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event" + id + " retained"}), 200
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/retain", methods=("DELETE",))
|
||||
def delete_retain(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
|
||||
)
|
||||
|
||||
event.retain_indefinitely = False
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event" + id + " un-retained"}), 200
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>", methods=("DELETE",))
|
||||
def delete_event(id):
|
||||
try:
|
||||
|
||||
@ -18,6 +18,7 @@ class Event(Model):
|
||||
region = JSONField()
|
||||
box = JSONField()
|
||||
area = IntegerField()
|
||||
retain_indefinitely = BooleanField(default=False)
|
||||
|
||||
|
||||
class Recordings(Model):
|
||||
|
||||
@ -15,7 +15,7 @@ from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
from peewee import JOIN, DoesNotExist
|
||||
from peewee import DoesNotExist
|
||||
|
||||
from frigate.config import RetainModeEnum, FrigateConfig
|
||||
from frigate.const import (
|
||||
|
||||
26
frigate/test/conftest.py
Normal file
26
frigate/test/conftest.py
Normal file
@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
from unittest import mock
|
||||
import sys
|
||||
|
||||
def fake_open(filename, *args, **kvargs):
|
||||
if filename == '/labelmap.txt':
|
||||
content = "0 person\n1 bicycle"
|
||||
else:
|
||||
raise FileNotFoundError(filename)
|
||||
file_object = mock.mock_open(read_data=content).return_value
|
||||
file_object.__iter__.return_value = content.splitlines(True)
|
||||
return file_object
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def filesystem_mock():
|
||||
with mock.patch("builtins.open", new=fake_open, create=True):
|
||||
yield
|
||||
|
||||
# monkeypatch tflite_runtime
|
||||
# in case of moving to the pytest completely, this can be done in more pyhonic way
|
||||
module = type(sys)('tflite_runtime')
|
||||
sys.modules['tflite_runtime'] = module
|
||||
|
||||
module = type(sys)('tflite_runtime.interpreter')
|
||||
module.load_delegate = mock.MagicMock()
|
||||
sys.modules['tflite_runtime.interpreter'] = module
|
||||
17
frigate/test/requirements.test.txt
Normal file
17
frigate/test/requirements.test.txt
Normal file
@ -0,0 +1,17 @@
|
||||
opencv-python-headless
|
||||
numpy
|
||||
imutils
|
||||
scipy
|
||||
psutil
|
||||
Flask
|
||||
paho-mqtt
|
||||
PyYAML
|
||||
matplotlib
|
||||
click
|
||||
setproctitle
|
||||
peewee
|
||||
peewee_migrate
|
||||
pydantic
|
||||
zeroconf
|
||||
ws4py
|
||||
pytest
|
||||
@ -622,7 +622,8 @@ def process_frames(
|
||||
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
|
||||
|
||||
for index in idxs:
|
||||
obj = group[index[0]]
|
||||
index = index if isinstance(index, np.int32) else index[0]
|
||||
obj = group[index]
|
||||
if clipped(obj, frame_shape):
|
||||
box = obj[2]
|
||||
# calculate a new region that will hopefully get the entire object
|
||||
|
||||
46
migrations/007_add_retain_indefinitely.py
Normal file
46
migrations/007_add_retain_indefinitely.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Peewee migrations -- 007_add_retain_indefinitely.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.python(func, *args, **kwargs) # Run python code
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
import peewee as pw
|
||||
from playhouse.sqlite_ext import *
|
||||
from decimal import ROUND_HALF_EVEN
|
||||
from frigate.models import Event
|
||||
|
||||
try:
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
SQL = pw.SQL
|
||||
|
||||
|
||||
def migrate(migrator, database, fake=False, **kwargs):
|
||||
migrator.add_fields(
|
||||
Event,
|
||||
retain_indefinitely=pw.BooleanField(default=False),
|
||||
)
|
||||
|
||||
|
||||
def rollback(migrator, database, fake=False, **kwargs):
|
||||
migrator.remove_fields(Event, ["retain_indefinitely"])
|
||||
@ -117,6 +117,24 @@ export function useDelete() {
|
||||
return deleteEvent;
|
||||
}
|
||||
|
||||
export function useRetain() {
|
||||
const { state } = useContext(Api);
|
||||
|
||||
async function retainEvent(eventId, shouldRetain) {
|
||||
if (!eventId) return null;
|
||||
|
||||
if (shouldRetain) {
|
||||
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'POST' });
|
||||
return await (response.status < 300 ? response.json() : { success: true });
|
||||
} else {
|
||||
const response = await fetch(`${state.host}/api/events/${eventId}/retain`, { method: 'DELETE' });
|
||||
return await (response.status < 300 ? response.json() : { success: true });
|
||||
}
|
||||
}
|
||||
|
||||
return retainEvent;
|
||||
}
|
||||
|
||||
export function useApiHost() {
|
||||
const { state } = useContext(Api);
|
||||
return state.host;
|
||||
|
||||
@ -17,6 +17,13 @@ const ButtonColors = {
|
||||
text:
|
||||
'text-red-500 hover:bg-red-500 hover:bg-opacity-20 focus:bg-red-500 focus:bg-opacity-40 active:bg-red-500 active:bg-opacity-40',
|
||||
},
|
||||
yellow: {
|
||||
contained: 'bg-yellow-500 focus:bg-yellow-400 active:bg-yellow-600 ring-yellow-300',
|
||||
outlined:
|
||||
'text-yellow-500 border-2 border-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
text:
|
||||
'text-yellow-500 hover:bg-yellow-500 hover:bg-opacity-20 focus:bg-yellow-500 focus:bg-opacity-40 active:bg-yellow-500 active:bg-opacity-40',
|
||||
},
|
||||
green: {
|
||||
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
|
||||
outlined:
|
||||
|
||||
12
web/src/icons/StarRecording.jsx
Normal file
12
web/src/icons/StarRecording.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function StarRecording({ className = '' }) {
|
||||
return (
|
||||
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6m.5 16.9L12 17.5 9.5 19l.7-2.8L8 14.3l2.9-.2 1.1-2.7 1.1 2.6 2.9.2-2.2 1.9.7 2.8M13 9V3.5L18.5 9H13z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(StarRecording);
|
||||
@ -7,16 +7,21 @@ import ArrowDown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
import Clip from '../icons/Clip';
|
||||
import Close from '../icons/Close';
|
||||
import StarRecording from '../icons/StarRecording';
|
||||
import Delete from '../icons/Delete';
|
||||
import Snapshot from '../icons/Snapshot';
|
||||
import Dialog from '../components/Dialog';
|
||||
import Heading from '../components/Heading';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
||||
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
||||
import { FetchStatus, useApiHost, useEvent, useDelete, useRetain } from '../api';
|
||||
|
||||
const ActionButtonGroup = ({ className, handleClickDelete, close }) => (
|
||||
const ActionButtonGroup = ({ className, isRetained, handleClickRetain, handleClickDelete, close }) => (
|
||||
<div className={`space-y-2 space-x-2 sm:space-y-0 xs:space-x-4 ${className}`}>
|
||||
<Button className="xs:w-auto" color={isRetained ? 'red' : 'yellow'} onClick={handleClickRetain}>
|
||||
<StarRecording className="w-6" />
|
||||
{isRetained ? ('Un-retain event') : ('Retain event')}
|
||||
</Button>
|
||||
<Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
|
||||
<Delete className="w-6" /> Delete event
|
||||
</Button>
|
||||
@ -54,6 +59,8 @@ export default function Event({ eventId, close, scrollRef }) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [shouldScroll, setShouldScroll] = useState(true);
|
||||
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
||||
const [isRetained, setIsRetained] = useState(false);
|
||||
const setRetainEvent = useRetain();
|
||||
const setDeleteEvent = useDelete();
|
||||
|
||||
useEffect(() => {
|
||||
@ -71,6 +78,22 @@ export default function Event({ eventId, close, scrollRef }) {
|
||||
};
|
||||
}, [data, scrollRef, eventId, shouldScroll]);
|
||||
|
||||
const handleClickRetain = useCallback(async () => {
|
||||
let success;
|
||||
try {
|
||||
success = await setRetainEvent(eventId, !isRetained);
|
||||
|
||||
if (success) {
|
||||
setIsRetained(!isRetained);
|
||||
|
||||
// Need to reload page otherwise retain button state won't stick if event is collapsed and re-opened.
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}, [eventId, isRetained, setRetainEvent]);
|
||||
|
||||
const handleClickDelete = () => {
|
||||
setShowDialog(true);
|
||||
};
|
||||
@ -98,6 +121,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
setIsRetained(data.retain_indefinitely);
|
||||
const startime = new Date(data.start_time * 1000);
|
||||
const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
|
||||
return (
|
||||
@ -119,7 +143,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
|
||||
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
|
||||
{showDialog ? (
|
||||
<Dialog
|
||||
onDismiss={handleDismissDeleteDialog}
|
||||
@ -210,7 +234,7 @@ export default function Event({ eventId, close, scrollRef }) {
|
||||
</div>
|
||||
<div className="space-y-2 xs:space-y-0">
|
||||
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" />
|
||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
|
||||
<ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ const TableHead = () => (
|
||||
<Th>Label</Th>
|
||||
<Th>Score</Th>
|
||||
<Th>Zones</Th>
|
||||
<Th>Retain</Th>
|
||||
<Th>Date</Th>
|
||||
<Th>Start</Th>
|
||||
<Th>End</Th>
|
||||
|
||||
@ -22,6 +22,7 @@ const EventsRow = memo(
|
||||
label,
|
||||
top_score: score,
|
||||
zones,
|
||||
retain_indefinitely
|
||||
}) => {
|
||||
const [viewEvent, setViewEvent] = useState(null);
|
||||
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
||||
@ -100,6 +101,7 @@ const EventsRow = memo(
|
||||
))}
|
||||
</ul>
|
||||
</Td>
|
||||
<Td>{retain_indefinitely ? 'True' : 'False'}</Td>
|
||||
<Td>{start.toLocaleDateString()}</Td>
|
||||
<Td>{start.toLocaleTimeString()}</Td>
|
||||
<Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user