Merge remote-tracking branch 'origin/release-0.11.0' into gstreamer

This commit is contained in:
YS 2022-02-22 08:53:56 +03:00
commit 1240a073c9
27 changed files with 423 additions and 17 deletions

View File

@ -55,7 +55,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build and run tests - 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: docker_tests_on_amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -67,4 +67,4 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build and run tests - 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"

View File

@ -3,7 +3,7 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version: version:
echo "VERSION='0.10.0-$(COMMIT_HASH)'" > frigate/version.py echo "VERSION='0.11.0-$(COMMIT_HASH)'" > frigate/version.py
web: web:
docker build --tag frigate-web --file docker/Dockerfile.web 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 @sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test
@echo "" >> docker/Dockerfile.test @echo "" >> docker/Dockerfile.test
@echo "RUN python3 -m unittest" >> 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 @rm docker/Dockerfile.test
.PHONY: web run_tests .PHONY: web run_tests

127
docker/Dockerfile Normal file
View 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"]

View File

@ -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. 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` ### `model`
If using a custom model, the width and height will need to be specified. If using a custom model, the width and height will need to be specified.

View File

@ -19,6 +19,34 @@ output_args:
rtmp: -c:v libx264 -an -f flv 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 ### RTMP Cameras
The input parameters need to be adjusted for RTMP cameras The input parameters need to be adjusted for RTMP cameras

View File

@ -238,6 +238,11 @@ motion:
# NOTE: Can be overridden at the camera level # NOTE: Can be overridden at the camera level
record: record:
# Optional: Enable recording (default: shown below) # 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 enabled: False
# Optional: Number of minutes to wait between cleanup runs (default: shown below) # 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 # This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o

View File

@ -5,7 +5,11 @@ title: Objects
import labels from "../../../labelmap.txt"; 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> <ul>
{labels.split("\n").map((label) => ( {labels.split("\n").map((label) => (

View File

@ -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. 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).

View File

@ -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. 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 ## Web Interface
### Prerequisites ### Prerequisites

View File

@ -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. 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 ### My mjpeg stream or snapshots look green and crazy

View File

@ -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. 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/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/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. - `/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 shm_size: "64mb" # update for your cameras based on calculation above
devices: devices:
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions - /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 - /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro

View File

@ -188,6 +188,14 @@ Returns data for a single event.
Permanently deletes the event along with any clips/snapshots. 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` ### `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. 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.

View File

@ -7,6 +7,7 @@ import sys
import threading import threading
from logging.handlers import QueueHandler from logging.handlers import QueueHandler
from typing import Dict, List from typing import Dict, List
import traceback import traceback
import yaml import yaml
from peewee_migrate import Router from peewee_migrate import Router

View File

@ -147,6 +147,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys), Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
# delete the media from disk # delete the media from disk
for event in expired_events: for event in expired_events:
@ -166,6 +167,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys), Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
update_query.execute() update_query.execute()
@ -192,6 +194,7 @@ class EventCleanup(threading.Thread):
Event.camera == name, Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
# delete the grabbed clips from disk # delete the grabbed clips from disk
for event in expired_events: for event in expired_events:
@ -210,6 +213,7 @@ class EventCleanup(threading.Thread):
Event.camera == name, Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label, Event.label == l.label,
Event.retain_indefinitely == False,
) )
update_query.execute() update_query.execute()

View File

@ -13,7 +13,6 @@ from functools import reduce
from pathlib import Path from pathlib import Path
import cv2 import cv2
from flask.helpers import send_file
import numpy as np import numpy as np
from flask import ( from flask import (
@ -26,10 +25,10 @@ from flask import (
request, request,
) )
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict 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.models import Event, Recordings
from frigate.stats import stats_snapshot from frigate.stats import stats_snapshot
from frigate.util import calculate_region from frigate.util import calculate_region
@ -120,6 +119,40 @@ def event(id):
return "Event not found", 404 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",)) @bp.route("/events/<id>", methods=("DELETE",))
def delete_event(id): def delete_event(id):
try: try:

View File

@ -18,6 +18,7 @@ class Event(Model):
region = JSONField() region = JSONField()
box = JSONField() box = JSONField()
area = IntegerField() area = IntegerField()
retain_indefinitely = BooleanField(default=False)
class Recordings(Model): class Recordings(Model):

View File

@ -15,7 +15,7 @@ from collections import defaultdict
from pathlib import Path from pathlib import Path
import psutil import psutil
from peewee import JOIN, DoesNotExist from peewee import DoesNotExist
from frigate.config import RetainModeEnum, FrigateConfig from frigate.config import RetainModeEnum, FrigateConfig
from frigate.const import ( from frigate.const import (

26
frigate/test/conftest.py Normal file
View 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

View 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

View File

@ -622,7 +622,8 @@ def process_frames(
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
for index in idxs: 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): if clipped(obj, frame_shape):
box = obj[2] box = obj[2]
# calculate a new region that will hopefully get the entire object # calculate a new region that will hopefully get the entire object

View 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"])

View File

@ -117,6 +117,24 @@ export function useDelete() {
return deleteEvent; 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() { export function useApiHost() {
const { state } = useContext(Api); const { state } = useContext(Api);
return state.host; return state.host;

View File

@ -17,6 +17,13 @@ const ButtonColors = {
text: 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', '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: { green: {
contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300', contained: 'bg-green-500 focus:bg-green-400 active:bg-green-600 ring-green-300',
outlined: outlined:

View 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);

View File

@ -7,16 +7,21 @@ import ArrowDown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup'; import ArrowDropup from '../icons/ArrowDropup';
import Clip from '../icons/Clip'; import Clip from '../icons/Clip';
import Close from '../icons/Close'; import Close from '../icons/Close';
import StarRecording from '../icons/StarRecording';
import Delete from '../icons/Delete'; import Delete from '../icons/Delete';
import Snapshot from '../icons/Snapshot'; import Snapshot from '../icons/Snapshot';
import Dialog from '../components/Dialog'; import Dialog from '../components/Dialog';
import Heading from '../components/Heading'; import Heading from '../components/Heading';
import VideoPlayer from '../components/VideoPlayer'; import VideoPlayer from '../components/VideoPlayer';
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table'; 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}`}> <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}> <Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
<Delete className="w-6" /> Delete event <Delete className="w-6" /> Delete event
</Button> </Button>
@ -54,6 +59,8 @@ export default function Event({ eventId, close, scrollRef }) {
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
const [shouldScroll, setShouldScroll] = useState(true); const [shouldScroll, setShouldScroll] = useState(true);
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE); const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
const [isRetained, setIsRetained] = useState(false);
const setRetainEvent = useRetain();
const setDeleteEvent = useDelete(); const setDeleteEvent = useDelete();
useEffect(() => { useEffect(() => {
@ -71,6 +78,22 @@ export default function Event({ eventId, close, scrollRef }) {
}; };
}, [data, scrollRef, eventId, shouldScroll]); }, [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 = () => { const handleClickDelete = () => {
setShowDialog(true); setShowDialog(true);
}; };
@ -98,6 +121,7 @@ export default function Event({ eventId, close, scrollRef }) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
setIsRetained(data.retain_indefinitely);
const startime = new Date(data.start_time * 1000); const startime = new Date(data.start_time * 1000);
const endtime = data.end_time ? new Date(data.end_time * 1000) : null; const endtime = data.end_time ? new Date(data.end_time * 1000) : null;
return ( return (
@ -119,7 +143,7 @@ export default function Event({ eventId, close, scrollRef }) {
)} )}
</Button> </Button>
</div> </div>
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" /> <ActionButtonGroup isRetained={isRetained} handleClickRetain={handleClickRetain} handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
{showDialog ? ( {showDialog ? (
<Dialog <Dialog
onDismiss={handleDismissDeleteDialog} onDismiss={handleDismissDeleteDialog}
@ -210,7 +234,7 @@ export default function Event({ eventId, close, scrollRef }) {
</div> </div>
<div className="space-y-2 xs:space-y-0"> <div className="space-y-2 xs:space-y-0">
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" /> <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>
</div> </div>
); );

View File

@ -9,6 +9,7 @@ const TableHead = () => (
<Th>Label</Th> <Th>Label</Th>
<Th>Score</Th> <Th>Score</Th>
<Th>Zones</Th> <Th>Zones</Th>
<Th>Retain</Th>
<Th>Date</Th> <Th>Date</Th>
<Th>Start</Th> <Th>Start</Th>
<Th>End</Th> <Th>End</Th>

View File

@ -22,6 +22,7 @@ const EventsRow = memo(
label, label,
top_score: score, top_score: score,
zones, zones,
retain_indefinitely
}) => { }) => {
const [viewEvent, setViewEvent] = useState(null); const [viewEvent, setViewEvent] = useState(null);
const { searchString, removeDefaultSearchKeys } = useSearchString(limit); const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
@ -100,6 +101,7 @@ const EventsRow = memo(
))} ))}
</ul> </ul>
</Td> </Td>
<Td>{retain_indefinitely ? 'True' : 'False'}</Td>
<Td>{start.toLocaleDateString()}</Td> <Td>{start.toLocaleDateString()}</Td>
<Td>{start.toLocaleTimeString()}</Td> <Td>{start.toLocaleTimeString()}</Td>
<Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td> <Td>{end === null ? 'In progress' : end.toLocaleTimeString()}</Td>