Compare commits

...

14 Commits

Author SHA1 Message Date
Blake Blackshear
1d45b0b351 update hwaccel docs 2022-06-11 06:42:09 -05:00
Blake Blackshear
ba119e4f96 revert back to ffmpeg 4.3.2 2022-06-10 07:38:26 -05:00
Blake Blackshear
75c2feb387 ffmpeg5 and intel va driver 2022-06-09 07:16:21 -05:00
Nicolas Mowen
da637d3c8f Limit size of player in events view (#3288)
* Set max width

* Set data-options so videojs accepts size changing

* Add comment to explain exmpty data-setup value

* Clarify comment
2022-06-02 07:06:39 -05:00
Blake Blackshear
bc078fcc88 remove deprecated max_seconds config option 2022-06-01 06:50:09 -05:00
Nicolas Mowen
5f9d477863 Update event filters naming and add sub label filter (#3194)
* Use default names so filters are more clear

* Add endpoint to get list of sub labels inside DB

* Fix crash on no internet

* Cleanups for sub_label http

* Add sub label selector to events UI

* Add event filtering for sub label

* Formatting files

* Reduce size of filters to fit on one line

* Add handler for tests

* Remove unused imports

* Only show the sub labels filter when there are sub labels in the DB

* Fix tests

* Use distinct instead of group_by

* Formatting

* Cleanup event logic
2022-05-29 09:47:43 -05:00
Nicolas Mowen
ca693240b1 Favorite events delete button warning (#3225)
* Add dialog to shield deletion of favorite event

* Use state to keep track of event id

* Adjust named

* Set color of button
2022-05-26 10:06:02 -05:00
Nicolas Mowen
468febc434 Catch crash on no internet (#3246) 2022-05-26 10:04:33 -05:00
Blake Blackshear
4b81c88794 use specific jellyfin-ffmpeg build 2022-05-26 10:02:43 -05:00
Blake Blackshear
2ac28b93f3 fix development port 2022-05-20 09:28:26 -05:00
Blake Blackshear
3e7ed982d4 fix tests 2022-05-20 09:28:26 -05:00
Nicolas Mowen
d8d410802f Set height and width of delete icon for firefox (#3226) 2022-05-19 07:41:31 -05:00
Blake Blackshear
ca7bad8909 get ingress to play nice with vite 2022-05-19 07:31:51 -05:00
Patrick Fruh
7b2b5bfa71 update hwaccel docs for nvidia docker-compose 2022-05-18 17:19:37 -05:00
21 changed files with 142 additions and 82 deletions

View File

@@ -23,6 +23,7 @@ services:
- /dev/bus/usb:/dev/bus/usb
ports:
- "1935:1935"
- "3000:3000"
- "5000:5000"
- "5001:5001"
- "8080:8080"

View File

@@ -46,6 +46,7 @@ RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
FROM debian:11-slim
ARG TARGETARCH
ARG JELLYFIN_FFMPEG_VERSION=4.3.2-1
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622
@@ -72,9 +73,8 @@ RUN apt-get -qq update \
&& 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 \
# jellyfin-ffmpeg
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
# enable non-free repo
&& sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list \
&& apt-get -qq update \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
# coral drivers
@@ -82,8 +82,11 @@ RUN apt-get -qq update \
&& pip3 install -U /wheels/*.whl \
# arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers jellyfin-ffmpeg; else \
# jellyfin-ffmpeg
wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers intel-media-va-driver-non-free ./jellyfin.deb \
&& rm jellyfin.deb; else \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
ffmpeg; \
fi \

View File

@@ -205,11 +205,13 @@ http {
add_header Cache-Control "public";
}
sub_filter 'href="/' 'href="$http_x_ingress_path/';
sub_filter 'url(/' 'url($http_x_ingress_path/';
sub_filter '"/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/js/' '"$http_x_ingress_path/js/';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
sub_filter '="/BASE_PATH/"' '=window.baseUrl';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
sub_filter_types text/css application/javascript;
sub_filter_once off;

View File

@@ -12,33 +12,21 @@ Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config
```yaml
ffmpeg:
hwaccel_args:
- -c:v
- h264_v4l2m2m
hwaccel_args: -c:v h264_v4l2m2m
```
### Intel-based CPUs (<10th Generation) via Quicksync
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
- -hwaccel_output_format
- yuv420p
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
```
### Intel-based CPUs (>=10th Generation) via Quicksync
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- qsv
- -qsv_device
- /dev/dri/renderD128
hwaccel_args: -c:v h264_qsv
```
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
@@ -47,11 +35,7 @@ ffmpeg:
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
```
### NVIDIA GPU
@@ -69,7 +53,9 @@ services:
resources:
reservations:
devices:
- capabilities: [gpu]
- driver: nvidia
count: 1
capabilities: [gpu]
```
The decoder you need to pass in the `hwaccel_args` will depend on the input video.
@@ -89,13 +75,11 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
For example, for H265 video (hevc), you'll select `hevc_cuvid`.
For example, for H264 video, you'll select `h264_cuvid`.
```yaml
ffmpeg:
hwaccel_args:
- -c:v
- hevc_cuvid
hwaccel_args: -c:v h264_cuvid
```
If everything is working correctly, you should see a significant improvement in performance.

View File

@@ -278,10 +278,6 @@ record:
mode: all
# Optional: Event recording settings
events:
# Optional: Maximum length of time to retain video during long events. (default: shown below)
# NOTE: If an object is being tracked for longer than this amount of time, the retained recordings
# will be the last x seconds of the event unless retain->days under record is > 0.
max_seconds: 300
# Optional: Number of seconds before the event to include (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include (default: shown below)

View File

@@ -108,6 +108,18 @@ ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffm
ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**VAAPI**
```shell
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**QSV**
```shell
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
## Web Interface
### Prerequisites

View File

@@ -83,7 +83,6 @@ class RetainConfig(FrigateBaseModel):
class EventsConfig(FrigateBaseModel):
max_seconds: int = Field(default=300, title="Maximum event duration.")
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
required_zones: List[str] = Field(

View File

@@ -2,18 +2,14 @@ import base64
from collections import OrderedDict
from datetime import datetime, timedelta
import copy
import json
import glob
import logging
import os
import re
import subprocess as sp
import time
from functools import reduce
from pathlib import Path
import cv2
from flask.helpers import send_file
import numpy as np
from flask import (
@@ -26,13 +22,12 @@ 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, PLUS_ENV_VAR
from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot
from frigate.util import calculate_region
from frigate.version import VERSION
logger = logging.getLogger(__name__)
@@ -251,6 +246,20 @@ def set_sub_label(id):
)
@bp.route("/sub_labels")
def get_sub_labels():
try:
events = Event.select(Event.sub_label).distinct()
except Exception as e:
return jsonify(
{"success": False, "message": f"Failed to get sub_labels: {e}"}, "404"
)
sub_labels = [e.sub_label for e in events]
sub_labels.remove(None)
return jsonify(sub_labels)
@bp.route("/events/<id>", methods=("DELETE",))
def delete_event(id):
try:
@@ -480,6 +489,7 @@ def events():
limit = request.args.get("limit", 100)
camera = request.args.get("camera", "all")
label = request.args.get("label", "all")
sub_label = request.args.get("sub_label", "all")
zone = request.args.get("zone", "all")
after = request.args.get("after", type=float)
before = request.args.get("before", type=float)
@@ -511,6 +521,9 @@ def events():
if label != "all":
clauses.append((Event.label == label))
if sub_label != "all":
clauses.append((Event.sub_label == sub_label))
if zone != "all":
clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))

View File

@@ -377,16 +377,11 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start all cameras.")
for camera, config in self.config.cameras.items():
logger.debug(f"Start camera: {camera}.")
# When deleting recordings without events, we have to keep at LEAST the configured max clip duration
min_end = (
datetime.datetime.now()
- datetime.timedelta(seconds=config.record.events.max_seconds)
).timestamp()
# Get the timestamp for cutoff of retained days
expire_days = config.record.retain.days
expire_before = (
expire_date = (
datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp()
expire_date = min(min_end, expire_before)
# Get recordings to check for expiration
recordings: Recordings = (

View File

@@ -20,9 +20,13 @@ logger = logging.getLogger(__name__)
def get_latest_version() -> str:
request = requests.get(
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
)
try:
request = requests.get(
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
)
except:
return "unknown"
response = request.json()
if request.ok and response and "tag_name" in response:

View File

@@ -2,7 +2,7 @@ import { rest } from 'msw';
import { API_HOST } from '../src/env';
export const handlers = [
rest.get(`${API_HOST}/api/config`, (req, res, ctx) => {
rest.get(`${API_HOST}api/config`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
@@ -35,7 +35,7 @@ export const handlers = [
})
);
}),
rest.get(`${API_HOST}/api/stats`, (req, res, ctx) => {
rest.get(`${API_HOST}api/stats`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
@@ -54,7 +54,7 @@ export const handlers = [
})
);
}),
rest.get(`${API_HOST}/api/events`, (req, res, ctx) => {
rest.get(`${API_HOST}api/events`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json(
@@ -72,4 +72,13 @@ export const handlers = [
)
);
}),
rest.get(`${API_HOST}api/sub_labels`, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
'one',
'two',
])
);
}),
];

View File

@@ -3,9 +3,9 @@
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"lint": "eslint ./ --ext .jsx,.js,.tsx,.ts",
"build": "tsc && vite build",
"build": "tsc && vite build --base=/BASE_PATH/",
"preview": "vite preview",
"test": "jest"
},

View File

@@ -1,2 +1,2 @@
export const ENV = 'test';
export const API_HOST = 'http://base-url.local:5000';
export const API_HOST = 'http://base-url.local:5000/';

View File

@@ -18,6 +18,6 @@ describe('useApiHost', () => {
<Test />
</ApiProvider>
);
expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
expect(screen.queryByText('http://base-url.local:5000/')).toBeInTheDocument();
});
});

View File

@@ -1,2 +1,2 @@
import { API_HOST } from '../env';
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || ''}`;
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;

View File

@@ -4,7 +4,7 @@ import useSWR, { SWRConfig } from 'swr';
import { MqttProvider } from './mqtt';
import axios from 'axios';
axios.defaults.baseURL = `${baseUrl}/api/`;
axios.defaults.baseURL = `${baseUrl}api/`;
export function ApiProvider({ children, options }) {
return (

View File

@@ -34,7 +34,7 @@ export function MqttProvider({
config,
children,
createWebsocket = defaultCreateWebsocket,
mqttUrl = `${baseUrl.replace(/^http/, 'ws')}/ws`,
mqttUrl = `${baseUrl.replace(/^http/, 'ws')}ws`,
}) {
const [state, dispatch] = useReducer(reducer, initialState);
const wsRef = useRef();

View File

@@ -5,7 +5,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
export default function JSMpegPlayer({ camera, width, height }) {
const playerRef = useRef();
const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`;
const url = `${baseUrl.replace(/^http/, 'ws')}live/${camera}`;
useEffect(() => {
const video = new JSMpeg.VideoElement(

View File

@@ -93,7 +93,8 @@ export default function VideoPlayer({ children, options, seekOptions = {}, onRea
return (
<div data-vjs-player>
<video ref={playerRef} className="small-player video-js vjs-default-skin" controls playsinline />
{/* Setting an empty data-setup is required to override the default values and allow video to be fit the size of its parent */}
<video ref={playerRef} className="small-player video-js vjs-default-skin" data-setup="{}" controls playsinline />
{children}
</div>
);

View File

@@ -1,2 +1,2 @@
export const ENV = import.meta.env.MODE;
export const API_HOST = ENV === "production" ? "" : "http://localhost:5000";
export const API_HOST = ENV === 'production' ? '' : 'http://localhost:5000/';

View File

@@ -44,6 +44,7 @@ export default function Events({ path, ...props }) {
camera: props.camera ?? 'all',
label: props.label ?? 'all',
zone: props.zone ?? 'all',
sub_label: props.sub_label ?? 'all',
});
const [state, setState] = useState({
showDownloadMenu: false,
@@ -59,6 +60,10 @@ export default function Events({ path, ...props }) {
has_snapshot: false,
plus_id: undefined,
});
const [deleteFavoriteState, setDeleteFavoriteState] = useState({
deletingFavoriteEventId: null,
showDeleteFavorite: false,
});
const eventsFetcher = useCallback((path, params) => {
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
@@ -82,6 +87,8 @@ export default function Events({ path, ...props }) {
const { data: config } = useSWR('config');
const { data: allSubLabels } = useSWR('sub_labels')
const filterValues = useMemo(
() => ({
cameras: Object.keys(config?.cameras || {}),
@@ -97,8 +104,9 @@ export default function Events({ path, ...props }) {
return memo;
}, config?.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i),
sub_labels: Object.values(allSubLabels || []),
}),
[config]
[config, allSubLabels]
);
const onSave = async (e, eventId, save) => {
@@ -114,11 +122,16 @@ export default function Events({ path, ...props }) {
}
};
const onDelete = async (e, eventId) => {
const onDelete = async (e, eventId, saved) => {
e.stopPropagation();
const response = await axios.delete(`events/${eventId}`);
if (response.status === 200) {
mutate();
if (saved) {
setDeleteFavoriteState({ deletingFavoriteEventId: eventId, showDeleteFavorite: true });
} else {
const response = await axios.delete(`events/${eventId}`);
if (response.status === 200) {
mutate();
}
}
};
@@ -231,11 +244,11 @@ export default function Events({ path, ...props }) {
<Heading>Events</Heading>
<div className="flex flex-wrap gap-2 items-center">
<select
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.camera}
onChange={(e) => onFilter('camera', e.target.value)}
>
<option value="all">all</option>
<option value="all">all cameras</option>
{filterValues.cameras.map((item) => (
<option key={item} value={item}>
{item}
@@ -243,11 +256,11 @@ export default function Events({ path, ...props }) {
))}
</select>
<select
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.label}
onChange={(e) => onFilter('label', e.target.value)}
>
<option value="all">all</option>
<option value="all">all labels</option>
{filterValues.labels.map((item) => (
<option key={item} value={item}>
{item}
@@ -255,17 +268,32 @@ export default function Events({ path, ...props }) {
))}
</select>
<select
className="basis-1/4 cursor-pointer rounded dark:bg-slate-800"
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.zone}
onChange={(e) => onFilter('zone', e.target.value)}
>
<option value="all">all</option>
<option value="all">all zones</option>
{filterValues.zones.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
{
filterValues.sub_labels.length > 0 && (
<select
className="basis-1/5 cursor-pointer rounded dark:bg-slate-800"
value={searchParams.sub_label}
onChange={(e) => onFilter('sub_label', e.target.value)}
>
<option value="all">all sub labels</option>
{filterValues.sub_labels.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
)}
<div ref={datePicker} className="ml-auto">
<CalendarIcon
className="h-8 w-8 cursor-pointer"
@@ -374,6 +402,19 @@ export default function Events({ path, ...props }) {
</div>
</Dialog>
)}
{deleteFavoriteState.showDeleteFavorite && (
<Dialog>
<div className="p-4">
<Heading size="lg">Delete Saved Event?</Heading>
<p className="mb-2">Confirm deletion of saved event.</p>
</div>
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
<Button className="ml-2" color="red" onClick={(e) => { setDeleteFavoriteState({ ...state, showDeleteFavorite: false }); onDelete(e, deleteFavoriteState.deletingFavoriteEventId, false) }} type="text">
Delete
</Button>
</div>
</Dialog>
)}
<div className="space-y-2">
{eventPages ? (
eventPages.map((page, i) => {
@@ -441,7 +482,7 @@ export default function Events({ path, ...props }) {
)}
</div>
<div class="flex flex-col">
<Delete className="cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id)} />
<Delete className="h-6 w-6 cursor-pointer" stroke="#f87171" onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)} />
<Download
className="h-6 w-6 mt-auto"
@@ -453,7 +494,7 @@ export default function Events({ path, ...props }) {
</div>
{viewEvent !== event.id ? null : (
<div className="space-y-4">
<div className="mx-auto">
<div className="mx-auto max-w-7xl">
{event.has_clip ? (
<>
<Heading size="lg">Clip</Heading>