mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 09:15:22 +03:00
Merge branch 'release-0.11.0' of https://github.com/blakeblackshear/frigate into reorder-cameras
This commit is contained in:
commit
deee4febde
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"]
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -120,6 +120,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):
|
||||
|
||||
@ -623,7 +623,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