forked from Github/frigate
Compare commits
20 Commits
v0.12.0-be
...
v0.12.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf2466c8c1 | ||
|
|
6b123675c4 | ||
|
|
ddcae2d4aa | ||
|
|
731db8fb8f | ||
|
|
cb0c5c2587 | ||
|
|
1643b4d108 | ||
|
|
acd1fb9e3e | ||
|
|
ddde477770 | ||
|
|
3edbb8dc41 | ||
|
|
581c2591ae | ||
|
|
d49359e26a | ||
|
|
e79eab711a | ||
|
|
5b7cd9ce64 | ||
|
|
3cb96091ec | ||
|
|
fdd2cc972e | ||
|
|
61243ad34b | ||
|
|
5f4c439f57 | ||
|
|
1f963ec5aa | ||
|
|
bcbf0061ff | ||
|
|
57dce4ec38 |
@@ -27,7 +27,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \
|
|||||||
FROM wget AS go2rtc
|
FROM wget AS go2rtc
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
WORKDIR /rootfs/usr/local/go2rtc/bin
|
WORKDIR /rootfs/usr/local/go2rtc/bin
|
||||||
RUN wget -qO go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/v0.1-rc.6/go2rtc_linux_${TARGETARCH}" \
|
RUN wget -qO go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/v0.1-rc.7/go2rtc_linux_${TARGETARCH}" \
|
||||||
&& chmod +x go2rtc
|
&& chmod +x go2rtc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ apt-get -qq install --no-install-recommends -y \
|
|||||||
unzip locales tzdata libxml2 xz-utils \
|
unzip locales tzdata libxml2 xz-utils \
|
||||||
python3-pip
|
python3-pip
|
||||||
|
|
||||||
# add raspberry pi repo
|
|
||||||
mkdir -p -m 600 /root/.gnupg
|
mkdir -p -m 600 /root/.gnupg
|
||||||
gpg --no-default-keyring --keyring /usr/share/keyrings/raspbian.gpg --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E
|
|
||||||
echo "deb [signed-by=/usr/share/keyrings/raspbian.gpg] http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list
|
|
||||||
|
|
||||||
# add coral repo
|
# add coral repo
|
||||||
wget --quiet -O /usr/share/keyrings/google-edgetpu.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
|
wget --quiet -O /usr/share/keyrings/google-edgetpu.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
|
||||||
@@ -30,21 +27,29 @@ apt-get -qq update
|
|||||||
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||||
libedgetpu1-max python3-tflite-runtime python3-pycoral
|
libedgetpu1-max python3-tflite-runtime python3-pycoral
|
||||||
|
|
||||||
# btbn-ffmpeg -> amd64 / arm64
|
# btbn-ffmpeg -> amd64
|
||||||
if [[ "${TARGETARCH}" == "amd64" || "${TARGETARCH}" == "arm64" ]]; then
|
if [[ "${TARGETARCH}" == "amd64" ]]; then
|
||||||
if [[ "${TARGETARCH}" == "amd64" ]]; then
|
|
||||||
btbn_arch="64"
|
|
||||||
else
|
|
||||||
btbn_arch="arm64"
|
|
||||||
fi
|
|
||||||
mkdir -p /usr/lib/btbn-ffmpeg
|
mkdir -p /usr/lib/btbn-ffmpeg
|
||||||
wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux${btbn_arch}-gpl-5.1.tar.xz"
|
wget -qO btbn-ffmpeg.tar.xz "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz"
|
||||||
tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1
|
tar -xf btbn-ffmpeg.tar.xz -C /usr/lib/btbn-ffmpeg --strip-components 1
|
||||||
rm -rf btbn-ffmpeg.tar.xz /usr/lib/btbn-ffmpeg/doc /usr/lib/btbn-ffmpeg/bin/ffplay
|
rm -rf btbn-ffmpeg.tar.xz /usr/lib/btbn-ffmpeg/doc /usr/lib/btbn-ffmpeg/bin/ffplay
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ffmpeg -> arm32
|
# ffmpeg -> arm32
|
||||||
if [[ "${TARGETARCH}" == "arm" ]]; then
|
if [[ "${TARGETARCH}" == "arm" ]]; then
|
||||||
|
# add raspberry pi repo
|
||||||
|
gpg --no-default-keyring --keyring /usr/share/keyrings/raspbian.gpg --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/raspbian.gpg] http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list
|
||||||
|
apt-get -qq update
|
||||||
|
apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ffmpeg -> arm64
|
||||||
|
if [[ "${TARGETARCH}" == "arm64" ]]; then
|
||||||
|
# add raspberry pi repo
|
||||||
|
gpg --no-default-keyring --keyring /usr/share/keyrings/raspbian.gpg --keyserver keyserver.ubuntu.com --recv-keys 82B129927FA3303E
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/raspbian.gpg] https://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list
|
||||||
|
apt-get -qq update
|
||||||
apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg
|
apt-get -qq install --no-install-recommends --no-install-suggests -y ffmpeg
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
log:
|
||||||
|
format: text
|
||||||
|
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555"
|
listen: ":8555"
|
||||||
candidates:
|
candidates:
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ mqtt:
|
|||||||
# NOTE: must be unique if you are running multiple instances
|
# NOTE: must be unique if you are running multiple instances
|
||||||
client_id: frigate
|
client_id: frigate
|
||||||
# Optional: user
|
# Optional: user
|
||||||
|
# NOTE: MQTT user can be specified with an environment variables that must begin with 'FRIGATE_'.
|
||||||
|
# e.g. user: '{FRIGATE_MQTT_USER}'
|
||||||
user: mqtt_user
|
user: mqtt_user
|
||||||
# Optional: password
|
# Optional: password
|
||||||
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
|
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ webRTC works by creating a websocket connection on extra ports. One of the follo
|
|||||||
* For local webRTC, you will need to create your own go2rtc config:
|
* For local webRTC, you will need to create your own go2rtc config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
log:
|
||||||
|
format: text
|
||||||
|
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555"
|
listen: ":8555"
|
||||||
candidates:
|
candidates:
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ id: record
|
|||||||
title: Recording
|
title: Recording
|
||||||
---
|
---
|
||||||
|
|
||||||
Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
|
Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM-DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
|
||||||
|
|
||||||
H265 recordings can be viewed in Edge and Safari only. All other browsers require recordings to be encoded with H264.
|
New recording segments are written from the camera stream to cache, they are only moved to disk if they match the setup recording retention policy.
|
||||||
|
|
||||||
|
H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other browsers require recordings to be encoded with H264.
|
||||||
|
|
||||||
## Will Frigate delete old recordings if my storage runs out?
|
## Will Frigate delete old recordings if my storage runs out?
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ Events from the database. Accepts the following query string parameters:
|
|||||||
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
|
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
|
||||||
| `has_clip` | int | Filter to events that have clips (0 or 1) |
|
| `has_clip` | int | Filter to events that have clips (0 or 1) |
|
||||||
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
|
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
|
||||||
|
| `in_progress` | int | Limit to events in progress (0 or 1) |
|
||||||
|
|
||||||
### `GET /api/events/summary`
|
### `GET /api/events/summary`
|
||||||
|
|
||||||
|
|||||||
@@ -863,8 +863,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
"""Merge camera config with globals."""
|
"""Merge camera config with globals."""
|
||||||
config = self.copy(deep=True)
|
config = self.copy(deep=True)
|
||||||
|
|
||||||
# MQTT password substitution
|
# MQTT user/password substitutions
|
||||||
if config.mqtt.password:
|
if config.mqtt.user or config.mqtt.password:
|
||||||
|
config.mqtt.user = config.mqtt.user.format(**FRIGATE_ENV_VARS)
|
||||||
config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)
|
config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)
|
||||||
|
|
||||||
# Global config to propagate down to camera level
|
# Global config to propagate down to camera level
|
||||||
@@ -968,7 +969,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
for _, camera in config.cameras.items():
|
for _, camera in config.cameras.items():
|
||||||
enabled_labels.update(camera.objects.track)
|
enabled_labels.update(camera.objects.track)
|
||||||
|
|
||||||
config.model.create_colormap(enabled_labels)
|
config.model.create_colormap(sorted(enabled_labels))
|
||||||
|
|
||||||
for key, detector in config.detectors.items():
|
for key, detector in config.detectors.items():
|
||||||
detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector)
|
detector_config: DetectorConfig = parse_obj_as(DetectorConfig, detector)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ YAML_EXT = (".yaml", ".yml")
|
|||||||
PLUS_ENV_VAR = "PLUS_API_KEY"
|
PLUS_ENV_VAR = "PLUS_API_KEY"
|
||||||
PLUS_API_HOST = "https://api.frigate.video"
|
PLUS_API_HOST = "https://api.frigate.video"
|
||||||
MAX_SEGMENT_DURATION = 600
|
MAX_SEGMENT_DURATION = 600
|
||||||
|
BTBN_PATH = "/usr/lib/btbn-ffmpeg"
|
||||||
|
|
||||||
# Regex Consts
|
# Regex Consts
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
"""Handles inserting and maintaining ffmpeg presets."""
|
"""Handles inserting and maintaining ffmpeg presets."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from frigate.version import VERSION
|
from frigate.version import VERSION
|
||||||
|
from frigate.const import BTBN_PATH
|
||||||
|
|
||||||
|
TIMEOUT_PARAM = "-timeout" if os.path.exists(BTBN_PATH) else "-stimeout"
|
||||||
|
|
||||||
_user_agent_args = [
|
_user_agent_args = [
|
||||||
"-user_agent",
|
"-user_agent",
|
||||||
@@ -75,6 +80,8 @@ PRESETS_HW_ACCEL_DECODE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PRESETS_HW_ACCEL_SCALE = {
|
PRESETS_HW_ACCEL_SCALE = {
|
||||||
|
"preset-rpi-32-h264": ["-f", "rawvideo", "-pix_fmt", "yuv420p"],
|
||||||
|
"preset-rpi-64-h264": ["-f", "rawvideo", "-pix_fmt", "yuv420p"],
|
||||||
"preset-vaapi": [
|
"preset-vaapi": [
|
||||||
"-vf",
|
"-vf",
|
||||||
"fps={},scale_vaapi=w={}:h={},hwdownload,format=yuv420p",
|
"fps={},scale_vaapi=w={}:h={},hwdownload,format=yuv420p",
|
||||||
@@ -83,13 +90,13 @@ PRESETS_HW_ACCEL_SCALE = {
|
|||||||
],
|
],
|
||||||
"preset-intel-qsv-h264": [
|
"preset-intel-qsv-h264": [
|
||||||
"-vf",
|
"-vf",
|
||||||
"vpp_qsv=framerate={}:scale_mode=1:w={}:h={}:detail=50:denoise=100:deinterlace=2:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
"vpp_qsv=framerate={},scale_qsv=w={}:h={}:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
||||||
"-f",
|
"-f",
|
||||||
"rawvideo",
|
"rawvideo",
|
||||||
],
|
],
|
||||||
"preset-intel-qsv-h265": [
|
"preset-intel-qsv-h265": [
|
||||||
"-vf",
|
"-vf",
|
||||||
"vpp_qsv=framerate={}:scale_mode=1:w={}:h={}:detail=50:denoise=100:deinterlace=2:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
"vpp_qsv=framerate={},scale_qsv=w={}:h={}:format=nv12,hwdownload,format=nv12,format=yuv420p",
|
||||||
"-f",
|
"-f",
|
||||||
"rawvideo",
|
"rawvideo",
|
||||||
],
|
],
|
||||||
@@ -114,6 +121,8 @@ PRESETS_HW_ACCEL_SCALE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PRESETS_HW_ACCEL_ENCODE = {
|
PRESETS_HW_ACCEL_ENCODE = {
|
||||||
|
"preset-rpi-32-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -g 50 -bf 0 {1}",
|
||||||
|
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m -g 50 -bf 0 {1}",
|
||||||
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||||
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||||
"preset-nvidia-h264": "ffmpeg -hide_banner {0} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {1}",
|
"preset-nvidia-h264": "ffmpeg -hide_banner {0} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {1}",
|
||||||
@@ -122,6 +131,8 @@ PRESETS_HW_ACCEL_ENCODE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PRESETS_HW_ACCEL_GO2RTC_ENGINE = {
|
PRESETS_HW_ACCEL_GO2RTC_ENGINE = {
|
||||||
|
"preset-rpi-32-h264": "v4l2m2m",
|
||||||
|
"preset-rpi-64-h264": "v4l2m2m",
|
||||||
"preset-intel-vaapi": "vaapi",
|
"preset-intel-vaapi": "vaapi",
|
||||||
"preset-intel-qsv-h264": "vaapi", # go2rtc doesn't support qsv
|
"preset-intel-qsv-h264": "vaapi", # go2rtc doesn't support qsv
|
||||||
"preset-intel-qsv-h265": "vaapi",
|
"preset-intel-qsv-h265": "vaapi",
|
||||||
@@ -258,7 +269,7 @@ PRESETS_INPUT = {
|
|||||||
"+genpts+discardcorrupt",
|
"+genpts+discardcorrupt",
|
||||||
"-rtsp_transport",
|
"-rtsp_transport",
|
||||||
"tcp",
|
"tcp",
|
||||||
"-timeout",
|
TIMEOUT_PARAM,
|
||||||
"5000000",
|
"5000000",
|
||||||
"-use_wallclock_as_timestamps",
|
"-use_wallclock_as_timestamps",
|
||||||
"1",
|
"1",
|
||||||
@@ -271,7 +282,7 @@ PRESETS_INPUT = {
|
|||||||
"+genpts+discardcorrupt",
|
"+genpts+discardcorrupt",
|
||||||
"-rtsp_transport",
|
"-rtsp_transport",
|
||||||
"udp",
|
"udp",
|
||||||
"-timeout",
|
TIMEOUT_PARAM,
|
||||||
"5000000",
|
"5000000",
|
||||||
"-use_wallclock_as_timestamps",
|
"-use_wallclock_as_timestamps",
|
||||||
"1",
|
"1",
|
||||||
@@ -290,7 +301,7 @@ PRESETS_INPUT = {
|
|||||||
"+genpts+discardcorrupt",
|
"+genpts+discardcorrupt",
|
||||||
"-rtsp_transport",
|
"-rtsp_transport",
|
||||||
"tcp",
|
"tcp",
|
||||||
"-timeout",
|
TIMEOUT_PARAM,
|
||||||
"5000000",
|
"5000000",
|
||||||
"-use_wallclock_as_timestamps",
|
"-use_wallclock_as_timestamps",
|
||||||
"1",
|
"1",
|
||||||
|
|||||||
@@ -565,6 +565,7 @@ def events():
|
|||||||
before = request.args.get("before", type=float)
|
before = request.args.get("before", type=float)
|
||||||
has_clip = request.args.get("has_clip", type=int)
|
has_clip = request.args.get("has_clip", type=int)
|
||||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||||
|
in_progress = request.args.get("in_progress", type=int)
|
||||||
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
|
||||||
favorites = request.args.get("favorites", type=int)
|
favorites = request.args.get("favorites", type=int)
|
||||||
|
|
||||||
@@ -602,8 +603,13 @@ def events():
|
|||||||
# for example a sub label 'bob' would get events
|
# for example a sub label 'bob' would get events
|
||||||
# with sub labels 'bob' and 'bob, john'
|
# with sub labels 'bob' and 'bob, john'
|
||||||
sub_label_clauses = []
|
sub_label_clauses = []
|
||||||
|
filtered_sub_labels = sub_labels.split(",")
|
||||||
|
|
||||||
for label in sub_labels.split(","):
|
if "None" in filtered_sub_labels:
|
||||||
|
filtered_sub_labels.remove("None")
|
||||||
|
sub_label_clauses.append((Event.sub_label.is_null()))
|
||||||
|
|
||||||
|
for label in filtered_sub_labels:
|
||||||
sub_label_clauses.append((Event.sub_label.cast("text") % f"*{label}*"))
|
sub_label_clauses.append((Event.sub_label.cast("text") % f"*{label}*"))
|
||||||
|
|
||||||
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
||||||
@@ -613,8 +619,13 @@ def events():
|
|||||||
# use matching so events with multiple zones
|
# use matching so events with multiple zones
|
||||||
# still match on a search where any zone matches
|
# still match on a search where any zone matches
|
||||||
zone_clauses = []
|
zone_clauses = []
|
||||||
|
filtered_zones = zones.split(",")
|
||||||
|
|
||||||
for zone in zones.split(","):
|
if "None" in filtered_zones:
|
||||||
|
filtered_zones.remove("None")
|
||||||
|
zone_clauses.append((Event.zones.length() == 0))
|
||||||
|
|
||||||
|
for zone in filtered_zones:
|
||||||
zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
||||||
|
|
||||||
zone_clause = reduce(operator.or_, zone_clauses)
|
zone_clause = reduce(operator.or_, zone_clauses)
|
||||||
@@ -632,6 +643,9 @@ def events():
|
|||||||
if not has_snapshot is None:
|
if not has_snapshot is None:
|
||||||
clauses.append((Event.has_snapshot == has_snapshot))
|
clauses.append((Event.has_snapshot == has_snapshot))
|
||||||
|
|
||||||
|
if not in_progress is None:
|
||||||
|
clauses.append((Event.end_time.is_null(in_progress)))
|
||||||
|
|
||||||
if not include_thumbnails:
|
if not include_thumbnails:
|
||||||
excluded_fields.append(Event.thumbnail)
|
excluded_fields.append(Event.thumbnail)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -278,9 +278,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
|
|
||||||
directory = os.path.join(
|
directory = os.path.join(
|
||||||
RECORD_DIR,
|
RECORD_DIR,
|
||||||
start_time.replace(tzinfo=datetime.timezone.utc)
|
start_time.astimezone(tz=datetime.timezone.utc).strftime("%Y-%m-%d/%H"),
|
||||||
.astimezone(tz=None)
|
|
||||||
.strftime("%Y-%m-%d/%H"),
|
|
||||||
camera,
|
camera,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ class RestreamApi:
|
|||||||
input.path.startswith("rtsp")
|
input.path.startswith("rtsp")
|
||||||
and not camera.restream.force_audio
|
and not camera.restream.force_audio
|
||||||
):
|
):
|
||||||
self.relays[cam_name] = escape_special_characters(input.path)
|
self.relays[
|
||||||
|
cam_name
|
||||||
|
] = f"{escape_special_characters(input.path)}#backchannel=0"
|
||||||
else:
|
else:
|
||||||
# go2rtc only supports rtsp for direct relay, otherwise ffmpeg is used
|
# go2rtc only supports rtsp for direct relay, otherwise ffmpeg is used
|
||||||
self.relays[cam_name] = get_manual_go2rtc_stream(
|
self.relays[cam_name] = get_manual_go2rtc_stream(
|
||||||
|
|||||||
@@ -738,12 +738,69 @@ def escape_special_characters(path: str) -> str:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_cgroups_version() -> str:
|
||||||
|
"""Determine what version of cgroups is enabled"""
|
||||||
|
|
||||||
|
stat_command = ["stat", "-fc", "%T", "/sys/fs/cgroup"]
|
||||||
|
|
||||||
|
p = sp.run(
|
||||||
|
stat_command,
|
||||||
|
encoding="ascii",
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if p.returncode == 0:
|
||||||
|
value: str = p.stdout.strip().lower()
|
||||||
|
|
||||||
|
if value == "cgroup2fs":
|
||||||
|
return "cgroup2"
|
||||||
|
elif value == "tmpfs":
|
||||||
|
return "cgroup"
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"Could not determine cgroups version: unhandled filesystem {value}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Could not determine cgroups version: {p.stderr}")
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def get_docker_memlimit_bytes() -> int:
|
||||||
|
"""Get mem limit in bytes set in docker if present. Returns -1 if no limit detected"""
|
||||||
|
|
||||||
|
# check running a supported cgroups version
|
||||||
|
if get_cgroups_version() == "cgroup2":
|
||||||
|
|
||||||
|
memlimit_command = ["cat", "/sys/fs/cgroup/memory.max"]
|
||||||
|
|
||||||
|
p = sp.run(
|
||||||
|
memlimit_command,
|
||||||
|
encoding="ascii",
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if p.returncode == 0:
|
||||||
|
value: str = p.stdout.strip()
|
||||||
|
|
||||||
|
if value.isnumeric():
|
||||||
|
return int(value)
|
||||||
|
elif value.lower() == "max":
|
||||||
|
return -1
|
||||||
|
else:
|
||||||
|
logger.debug(f"Unable to get docker memlimit: {p.stderr}")
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def get_cpu_stats() -> dict[str, dict]:
|
def get_cpu_stats() -> dict[str, dict]:
|
||||||
"""Get cpu usages for each process id"""
|
"""Get cpu usages for each process id"""
|
||||||
usages = {}
|
usages = {}
|
||||||
# -n=2 runs to ensure extraneous values are not included
|
# -n=2 runs to ensure extraneous values are not included
|
||||||
top_command = ["top", "-b", "-n", "2"]
|
top_command = ["top", "-b", "-n", "2"]
|
||||||
|
|
||||||
|
docker_memlimit = get_docker_memlimit_bytes() / 1024
|
||||||
|
|
||||||
p = sp.run(
|
p = sp.run(
|
||||||
top_command,
|
top_command,
|
||||||
encoding="ascii",
|
encoding="ascii",
|
||||||
@@ -759,9 +816,18 @@ def get_cpu_stats() -> dict[str, dict]:
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
stats = list(filter(lambda a: a != "", line.strip().split(" ")))
|
stats = list(filter(lambda a: a != "", line.strip().split(" ")))
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
if docker_memlimit > 0:
|
||||||
|
mem_res = int(stats[5])
|
||||||
|
mem_pct = str(
|
||||||
|
round((float(mem_res) / float(docker_memlimit)) * 100, 1)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mem_pct = stats[9]
|
||||||
|
|
||||||
usages[stats[0]] = {
|
usages[stats[0]] = {
|
||||||
"cpu": stats[8],
|
"cpu": stats[8],
|
||||||
"mem": stats[9],
|
"mem": mem_pct,
|
||||||
}
|
}
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
@@ -780,7 +846,7 @@ def get_amd_gpu_stats() -> dict[str, str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
logger.error(p.stderr)
|
logger.error(f"Unable to poll radeon GPU stats: {p.stderr}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
usages = p.stdout.split(",")
|
usages = p.stdout.split(",")
|
||||||
@@ -816,7 +882,7 @@ def get_intel_gpu_stats() -> dict[str, str]:
|
|||||||
|
|
||||||
# timeout has a non-zero returncode when timeout is reached
|
# timeout has a non-zero returncode when timeout is reached
|
||||||
if p.returncode != 124:
|
if p.returncode != 124:
|
||||||
logger.error(p.stderr)
|
logger.error(f"Unable to poll intel GPU stats: {p.stderr}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
reading = "".join(p.stdout.split())
|
reading = "".join(p.stdout.split())
|
||||||
@@ -866,7 +932,7 @@ def get_nvidia_gpu_stats() -> dict[str, str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
logger.error(p.stderr)
|
logger.error(f"Unable to poll nvidia GPU stats: {p.stderr}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
usages = p.stdout.split("\n")[1].strip().split(",")
|
usages = p.stdout.split("\n")[1].strip().split(",")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cycjimmy/jsmpeg-player": "^6.0.5",
|
"@cycjimmy/jsmpeg-player": "^6.0.5",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.2.2",
|
||||||
|
"copy-to-clipboard": "3.3.3",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"idb-keyval": "^6.2.0",
|
"idb-keyval": "^6.2.0",
|
||||||
"immer": "^9.0.16",
|
"immer": "^9.0.16",
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { useCallback, useState } from 'preact/hooks';
|
|||||||
|
|
||||||
export default function ButtonsTabbed({
|
export default function ButtonsTabbed({
|
||||||
viewModes = [''],
|
viewModes = [''],
|
||||||
|
currentViewMode = '',
|
||||||
setViewMode = null,
|
setViewMode = null,
|
||||||
setHeader = null,
|
setHeader = null,
|
||||||
headers = [''],
|
headers = [''],
|
||||||
className = 'text-gray-600 py-0 px-4 block hover:text-gray-500',
|
className = 'text-gray-600 py-0 px-4 block hover:text-gray-500',
|
||||||
selectedClassName = `${className} focus:outline-none border-b-2 font-medium border-gray-500`,
|
selectedClassName = `${className} focus:outline-none border-b-2 font-medium border-gray-500`,
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState(0);
|
const [selected, setSelected] = useState(viewModes ? viewModes.indexOf(currentViewMode) : 0);
|
||||||
const captitalize = (str) => {
|
const captitalize = (str) => {
|
||||||
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
|
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function Camera({ camera }) {
|
|||||||
? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height))
|
||||||
: 0;
|
: 0;
|
||||||
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'mse');
|
const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'mse');
|
||||||
const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['mse'];
|
const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['mse', 'webrtc', 'jsmpeg'] : ['jsmpeg'];
|
||||||
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject);
|
||||||
|
|
||||||
const handleSetOption = useCallback(
|
const handleSetOption = useCallback(
|
||||||
@@ -77,7 +77,7 @@ export default function Camera({ camera }) {
|
|||||||
labelPosition="after"
|
labelPosition="after"
|
||||||
/>
|
/>
|
||||||
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
|
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
|
||||||
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Masks" labelPosition="after" />
|
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Motion Masks" labelPosition="after" />
|
||||||
<Switch
|
<Switch
|
||||||
checked={options['motion']}
|
checked={options['motion']}
|
||||||
id="motion"
|
id="motion"
|
||||||
@@ -98,7 +98,7 @@ export default function Camera({ camera }) {
|
|||||||
|
|
||||||
let player;
|
let player;
|
||||||
if (viewMode === 'live') {
|
if (viewMode === 'live') {
|
||||||
if (viewSource == 'mse') {
|
if (viewSource == 'mse' && cameraConfig.restream.enabled) {
|
||||||
if (videojs.browser.IS_IOS) {
|
if (videojs.browser.IS_IOS) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -116,7 +116,7 @@ export default function Camera({ camera }) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (viewSource == 'webrtc') {
|
} else if (viewSource == 'webrtc' && cameraConfig.restream.enabled) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="max-w-5xl">
|
<div className="max-w-5xl">
|
||||||
@@ -170,7 +170,7 @@ export default function Camera({ camera }) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonsTabbed viewModes={['live', 'debug']} setViewMode={setViewMode} />
|
<ButtonsTabbed viewModes={['live', 'debug']} currentViewMode={viewMode} setViewMode={setViewMode} />
|
||||||
|
|
||||||
{player}
|
{player}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useState } from 'preact/hooks';
|
|||||||
import Button from '../components/Button';
|
import Button from '../components/Button';
|
||||||
import { editor, Uri } from 'monaco-editor';
|
import { editor, Uri } from 'monaco-editor';
|
||||||
import { setDiagnosticsOptions } from 'monaco-yaml';
|
import { setDiagnosticsOptions } from 'monaco-yaml';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
export default function Config() {
|
export default function Config() {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
@@ -40,7 +41,7 @@ export default function Config() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyConfig = async () => {
|
const handleCopyConfig = async () => {
|
||||||
await window.navigator.clipboard.writeText(window.editor.getValue());
|
copy(window.editor.getValue());
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function Logs() {
|
|||||||
<div className="space-y-4 p-2 px-4">
|
<div className="space-y-4 p-2 px-4">
|
||||||
<Heading>Logs</Heading>
|
<Heading>Logs</Heading>
|
||||||
|
|
||||||
<ButtonsTabbed viewModes={['frigate', 'go2rtc', 'nginx']} setViewMode={setLogService} />
|
<ButtonsTabbed viewModes={['frigate', 'go2rtc', 'nginx']} currentViewMode={logService} setViewMode={setLogService} />
|
||||||
|
|
||||||
<Button className="" onClick={handleCopyLogs}>
|
<Button className="" onClick={handleCopyLogs}>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import axios from 'axios';
|
|||||||
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
import { Table, Tbody, Thead, Tr, Th, Td } from '../components/Table';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import Dialog from '../components/Dialog';
|
import Dialog from '../components/Dialog';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
const emptyObject = Object.freeze({});
|
const emptyObject = Object.freeze({});
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ export default function System() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCopyFfprobe = async () => {
|
const onCopyFfprobe = async () => {
|
||||||
await window.navigator.clipboard.writeText(JSON.stringify(state.ffprobe, null, 2));
|
copy(JSON.stringify(state.ffprobe, null, 2));
|
||||||
setState({ ...state, ffprobe: '', showFfprobe: false });
|
setState({ ...state, ffprobe: '', showFfprobe: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export default function System() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCopyVainfo = async () => {
|
const onCopyVainfo = async () => {
|
||||||
await window.navigator.clipboard.writeText(JSON.stringify(state.vaifp, null, 2));
|
copy(JSON.stringify(state.vainfo, null, 2));
|
||||||
setState({ ...state, vainfo: '', showVainfo: false });
|
setState({ ...state, vainfo: '', showVainfo: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,15 +142,17 @@ export default function System() {
|
|||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>P-ID</Th>
|
<Th>P-ID</Th>
|
||||||
<Th>Detection Start</Th>
|
|
||||||
<Th>Inference Speed</Th>
|
<Th>Inference Speed</Th>
|
||||||
|
<Th>CPU %</Th>
|
||||||
|
<Th>Memory %</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>{detectors[detector]['pid']}</Td>
|
<Td>{detectors[detector]['pid']}</Td>
|
||||||
<Td>{detectors[detector]['detection_start']}</Td>
|
<Td>{detectors[detector]['inference_speed']} ms</Td>
|
||||||
<Td>{detectors[detector]['inference_speed']}</Td>
|
<Td>{cpu_usages[detectors[detector]['pid']]?.['cpu'] || '- '}%</Td>
|
||||||
|
<Td>{cpu_usages[detectors[detector]['pid']]?.['mem'] || '- '}%</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -184,7 +187,7 @@ export default function System() {
|
|||||||
<Table className="w-full">
|
<Table className="w-full">
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Gpu %</Th>
|
<Th>GPU %</Th>
|
||||||
<Th>Memory %</Th>
|
<Th>Memory %</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -219,20 +222,27 @@ export default function System() {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Process</Th>
|
<Th>Process</Th>
|
||||||
<Th>P-ID</Th>
|
<Th>P-ID</Th>
|
||||||
<Th>fps</Th>
|
<Th>FPS</Th>
|
||||||
<Th>Cpu %</Th>
|
<Th>CPU %</Th>
|
||||||
<Th>Memory %</Th>
|
<Th>Memory %</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr key="capture" index="0">
|
<Tr key="ffmpeg" index="0">
|
||||||
|
<Td>ffmpeg</Td>
|
||||||
|
<Td>{cameras[camera]['ffmpeg_pid'] || '- '}</Td>
|
||||||
|
<Td>{cameras[camera]['camera_fps'] || '- '}</Td>
|
||||||
|
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cpu'] || '- '}%</Td>
|
||||||
|
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['mem'] || '- '}%</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr key="capture" index="1">
|
||||||
<Td>Capture</Td>
|
<Td>Capture</Td>
|
||||||
<Td>{cameras[camera]['capture_pid'] || '- '}</Td>
|
<Td>{cameras[camera]['capture_pid'] || '- '}</Td>
|
||||||
<Td>{cameras[camera]['process_fps'] || '- '}</Td>
|
<Td>{cameras[camera]['process_fps'] || '- '}</Td>
|
||||||
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['cpu'] || '- '}%</Td>
|
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['cpu'] || '- '}%</Td>
|
||||||
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['mem'] || '- '}%</Td>
|
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['mem'] || '- '}%</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr key="detect" index="1">
|
<Tr key="detect" index="2">
|
||||||
<Td>Detect</Td>
|
<Td>Detect</Td>
|
||||||
<Td>{cameras[camera]['pid'] || '- '}</Td>
|
<Td>{cameras[camera]['pid'] || '- '}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -241,13 +251,6 @@ export default function System() {
|
|||||||
<Td>{cpu_usages[cameras[camera]['pid']]?.['cpu'] || '- '}%</Td>
|
<Td>{cpu_usages[cameras[camera]['pid']]?.['cpu'] || '- '}%</Td>
|
||||||
<Td>{cpu_usages[cameras[camera]['pid']]?.['mem'] || '- '}%</Td>
|
<Td>{cpu_usages[cameras[camera]['pid']]?.['mem'] || '- '}%</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr key="ffmpeg" index="2">
|
|
||||||
<Td>ffmpeg</Td>
|
|
||||||
<Td>{cameras[camera]['ffmpeg_pid'] || '- '}</Td>
|
|
||||||
<Td>{cameras[camera]['camera_fps'] || '- '}</Td>
|
|
||||||
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cpu'] || '- '}%</Td>
|
|
||||||
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['mem'] || '- '}%</Td>
|
|
||||||
</Tr>
|
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user