diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 4ed9b46c7..131863e97 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -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"
diff --git a/Makefile b/Makefile
index ee1d7899f..cc4ac2d2d 100644
--- a/Makefile
+++ b/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
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 000000000..9363a8fb3
--- /dev/null
+++ b/docker/Dockerfile
@@ -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"]
diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md
index 18521b7d5..790b494f7 100644
--- a/docs/docs/configuration/advanced.md
+++ b/docs/docs/configuration/advanced.md
@@ -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.
diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md
index a82a58f84..465e0a5ce 100644
--- a/docs/docs/configuration/camera_specific.md
+++ b/docs/docs/configuration/camera_specific.md
@@ -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
diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md
index e334d88ee..5d8638745 100644
--- a/docs/docs/configuration/index.md
+++ b/docs/docs/configuration/index.md
@@ -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
diff --git a/docs/docs/configuration/objects.mdx b/docs/docs/configuration/objects.mdx
index fd42a4399..1d236bf42 100644
--- a/docs/docs/configuration/objects.mdx
+++ b/docs/docs/configuration/objects.mdx
@@ -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.
{labels.split("\n").map((label) => (
diff --git a/docs/docs/configuration/rtmp.md b/docs/docs/configuration/rtmp.md
index af5b0f5f8..76f6f81a7 100644
--- a/docs/docs/configuration/rtmp.md
+++ b/docs/docs/configuration/rtmp.md
@@ -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:///live/`. 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).
diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md
index a818000b4..16e0ce71e 100644
--- a/docs/docs/contributing.md
+++ b/docs/docs/contributing.md
@@ -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
diff --git a/docs/docs/faqs.md b/docs/docs/faqs.md
index 468cd4f00..34b040f01 100644
--- a/docs/docs/faqs.md
+++ b/docs/docs/faqs.md
@@ -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
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
index fc236f3f7..b2c1d65bf 100644
--- a/docs/docs/installation.md
+++ b/docs/docs/installation.md
@@ -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
diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md
index 69b6e7632..b6fc4e601 100644
--- a/docs/docs/integrations/api.md
+++ b/docs/docs/integrations/api.md
@@ -188,6 +188,14 @@ Returns data for a single event.
Permanently deletes the event along with any clips/snapshots.
+### `POST /api/events//retain`
+
+Sets retain to true for the event id.
+
+### `DELETE /api/events//retain`
+
+Sets retain to false for the event id (event may be deleted quickly after removing).
+
### `GET /api/events//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.
diff --git a/frigate/app.py b/frigate/app.py
index 46ff9dc41..d48e30fea 100644
--- a/frigate/app.py
+++ b/frigate/app.py
@@ -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
diff --git a/frigate/events.py b/frigate/events.py
index e92a37f56..b90204361 100644
--- a/frigate/events.py
+++ b/frigate/events.py
@@ -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()
diff --git a/frigate/http.py b/frigate/http.py
index 80fce55e3..54115b15c 100644
--- a/frigate/http.py
+++ b/frigate/http.py
@@ -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//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//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/", methods=("DELETE",))
def delete_event(id):
try:
diff --git a/frigate/models.py b/frigate/models.py
index 35a397f5c..ff83dcb54 100644
--- a/frigate/models.py
+++ b/frigate/models.py
@@ -18,6 +18,7 @@ class Event(Model):
region = JSONField()
box = JSONField()
area = IntegerField()
+ retain_indefinitely = BooleanField(default=False)
class Recordings(Model):
diff --git a/frigate/record.py b/frigate/record.py
index ebd9659de..8d5bc6d68 100644
--- a/frigate/record.py
+++ b/frigate/record.py
@@ -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 (
diff --git a/frigate/test/conftest.py b/frigate/test/conftest.py
new file mode 100644
index 000000000..5b8986f62
--- /dev/null
+++ b/frigate/test/conftest.py
@@ -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
diff --git a/frigate/test/requirements.test.txt b/frigate/test/requirements.test.txt
new file mode 100644
index 000000000..910e0ab0b
--- /dev/null
+++ b/frigate/test/requirements.test.txt
@@ -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
\ No newline at end of file
diff --git a/frigate/video.py b/frigate/video.py
index 9d322e633..ea624c90d 100755
--- a/frigate/video.py
+++ b/frigate/video.py
@@ -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
diff --git a/migrations/007_add_retain_indefinitely.py b/migrations/007_add_retain_indefinitely.py
new file mode 100644
index 000000000..a46b72e29
--- /dev/null
+++ b/migrations/007_add_retain_indefinitely.py
@@ -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"])
diff --git a/web/src/api/index.jsx b/web/src/api/index.jsx
index f6ef556d8..a2f7d1d75 100644
--- a/web/src/api/index.jsx
+++ b/web/src/api/index.jsx
@@ -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;
diff --git a/web/src/components/Button.jsx b/web/src/components/Button.jsx
index 031010dae..9cbb14a1a 100644
--- a/web/src/components/Button.jsx
+++ b/web/src/components/Button.jsx
@@ -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:
diff --git a/web/src/icons/StarRecording.jsx b/web/src/icons/StarRecording.jsx
new file mode 100644
index 000000000..eebf8e4aa
--- /dev/null
+++ b/web/src/icons/StarRecording.jsx
@@ -0,0 +1,12 @@
+import { h } from 'preact';
+import { memo } from 'preact/compat';
+
+export function StarRecording({ className = '' }) {
+ return (
+
+ );
+}
+
+export default memo(StarRecording);
diff --git a/web/src/routes/Event.jsx b/web/src/routes/Event.jsx
index 48e53e4e9..2b58bc698 100644
--- a/web/src/routes/Event.jsx
+++ b/web/src/routes/Event.jsx
@@ -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 }) => (