forked from Github/frigate
Compare commits
1 Commits
update_cal
...
handle_inv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e954891135 |
@@ -1,4 +1,4 @@
|
|||||||
# syntax=docker/dockerfile:1.6
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
@@ -121,15 +121,13 @@ RUN apt-get -qq update \
|
|||||||
apt-transport-https \
|
apt-transport-https \
|
||||||
gnupg \
|
gnupg \
|
||||||
wget \
|
wget \
|
||||||
# the key fingerprint can be obtained from https://ftp-master.debian.org/keys.html
|
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 648ACFD622F3D138 \
|
||||||
&& wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xA4285295FC7B1A81600062A9605C66F00D6C9793" | \
|
&& echo "deb http://deb.debian.org/debian bullseye main contrib non-free" | tee /etc/apt/sources.list.d/raspi.list \
|
||||||
gpg --dearmor > /usr/share/keyrings/debian-archive-bullseye-stable.gpg \
|
|
||||||
&& echo "deb [signed-by=/usr/share/keyrings/debian-archive-bullseye-stable.gpg] http://deb.debian.org/debian bullseye main contrib non-free" | \
|
|
||||||
tee /etc/apt/sources.list.d/debian-bullseye-nonfree.list \
|
|
||||||
&& apt-get -qq update \
|
&& apt-get -qq update \
|
||||||
&& apt-get -qq install -y \
|
&& apt-get -qq install -y \
|
||||||
python3.9 \
|
python3.9 \
|
||||||
python3.9-dev \
|
python3.9-dev \
|
||||||
|
wget \
|
||||||
# opencv dependencies
|
# opencv dependencies
|
||||||
build-essential cmake git pkg-config libgtk-3-dev \
|
build-essential cmake git pkg-config libgtk-3-dev \
|
||||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||||
@@ -201,9 +199,6 @@ ENV S6_LOGGING_SCRIPT="T 1 n0 s10000000 T"
|
|||||||
ENTRYPOINT ["/init"]
|
ENTRYPOINT ["/init"]
|
||||||
CMD []
|
CMD []
|
||||||
|
|
||||||
HEALTHCHECK --start-period=120s --start-interval=5s --interval=15s --timeout=5s --retries=3 \
|
|
||||||
CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1
|
|
||||||
|
|
||||||
# Frigate deps with Node.js and NPM for devcontainer
|
# Frigate deps with Node.js and NPM for devcontainer
|
||||||
FROM deps AS devcontainer
|
FROM deps AS devcontainer
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ http {
|
|||||||
secure_token $args;
|
secure_token $args;
|
||||||
secure_token_types application/vnd.apple.mpegurl;
|
secure_token_types application/vnd.apple.mpegurl;
|
||||||
|
|
||||||
|
add_header Access-Control-Allow-Headers '*';
|
||||||
|
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
|
||||||
|
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
|
||||||
|
add_header Access-Control-Allow-Origin '*';
|
||||||
add_header Cache-Control "no-store";
|
add_header Cache-Control "no-store";
|
||||||
expires off;
|
expires off;
|
||||||
}
|
}
|
||||||
@@ -100,6 +104,16 @@ http {
|
|||||||
location /stream/ {
|
location /stream/ {
|
||||||
add_header Cache-Control "no-store";
|
add_header Cache-Control "no-store";
|
||||||
expires off;
|
expires off;
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
types {
|
types {
|
||||||
application/dash+xml mpd;
|
application/dash+xml mpd;
|
||||||
@@ -112,6 +126,16 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /clips/ {
|
location /clips/ {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
types {
|
types {
|
||||||
video/mp4 mp4;
|
video/mp4 mp4;
|
||||||
@@ -128,6 +152,17 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /recordings/ {
|
location /recordings/ {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
types {
|
types {
|
||||||
video/mp4 mp4;
|
video/mp4 mp4;
|
||||||
}
|
}
|
||||||
@@ -138,6 +173,17 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /exports/ {
|
location /exports/ {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
types {
|
types {
|
||||||
video/mp4 mp4;
|
video/mp4 mp4;
|
||||||
}
|
}
|
||||||
@@ -189,6 +235,8 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ~* /api/.*\.(jpg|jpeg|png)$ {
|
location ~* /api/.*\.(jpg|jpeg|png)$ {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||||
rewrite ^/api/(.*)$ $1 break;
|
rewrite ^/api/(.*)$ $1 break;
|
||||||
proxy_pass http://frigate_api;
|
proxy_pass http://frigate_api;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
@@ -200,6 +248,10 @@ http {
|
|||||||
location /api/ {
|
location /api/ {
|
||||||
add_header Cache-Control "no-store";
|
add_header Cache-Control "no-store";
|
||||||
expires off;
|
expires off;
|
||||||
|
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||||
proxy_pass http://frigate_api/;
|
proxy_pass http://frigate_api/;
|
||||||
proxy_pass_request_headers on;
|
proxy_pass_request_headers on;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# syntax=docker/dockerfile:1.6
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
@@ -24,6 +24,3 @@ COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_laye
|
|||||||
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
||||||
COPY docker/tensorrt/detector/rootfs/ /
|
COPY docker/tensorrt/detector/rootfs/ /
|
||||||
ENV YOLO_MODELS="yolov7-320"
|
ENV YOLO_MODELS="yolov7-320"
|
||||||
|
|
||||||
HEALTHCHECK --start-period=600s --start-interval=5s --interval=15s --timeout=5s --retries=3 \
|
|
||||||
CMD curl --fail --silent --show-error http://127.0.0.1:5000/api/version || exit 1
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ cameras:
|
|||||||
|
|
||||||
### Configuring Minimum Volume
|
### Configuring Minimum Volume
|
||||||
|
|
||||||
The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. MQTT explorer can be used on the audio topic to see what volume level is being detected.
|
The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widelely between camera models so it is important to run tests to see what volume levels are. MQTT explorer can be used on the audio topic to see what volume level is being detected.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ Each role can only be assigned to one input per camera. The options for roles ar
|
|||||||
|
|
||||||
| Role | Description |
|
| Role | Description |
|
||||||
| -------- | ---------------------------------------------------------------------------------------- |
|
| -------- | ---------------------------------------------------------------------------------------- |
|
||||||
| `detect` | Main feed for object detection. [docs](object_detectors.md) |
|
| `detect` | Main feed for object detection |
|
||||||
| `record` | Saves segments of the video feed based on configuration settings. [docs](record.md) |
|
| `record` | Saves segments of the video feed based on configuration settings. [docs](record.md) |
|
||||||
| `audio` | Feed for audio based detection. [docs](audio_detectors.md) |
|
|
||||||
| `rtmp` | Deprecated: Broadcast as an RTMP feed for other services to consume. [docs](restream.md) |
|
| `rtmp` | Deprecated: Broadcast as an RTMP feed for other services to consume. [docs](restream.md) |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -54,7 +53,7 @@ For camera model specific settings check the [camera specific](camera_specific.m
|
|||||||
|
|
||||||
:::caution
|
:::caution
|
||||||
|
|
||||||
Not every PTZ supports ONVIF, which is the standard protocol Frigate uses to communicate with your camera. Check the [official list of ONVIF conformant products](https://www.onvif.org/conformant-products/), your camera documentation, or camera manufacturer's website to ensure your PTZ supports ONVIF. Also, ensure your camera is running the latest firmware.
|
Not every PTZ supports ONVIF, which is the standard protocol Frigate uses to communicate with your camera. Check your camera documentation or manufacturer's website to ensure your camera supports ONVIF. If your camera supports ONVIF and you continue to have trouble, make sure your camera is running the latest firmware.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -79,17 +78,12 @@ An ONVIF-capable camera that supports relative movement within the field of view
|
|||||||
|
|
||||||
This list of working and non-working PTZ cameras is based on user feedback.
|
This list of working and non-working PTZ cameras is based on user feedback.
|
||||||
|
|
||||||
| Brand or specific camera | PTZ Controls | Autotracking | Notes |
|
| Brand or specific camera | PTZ Controls | Autotracking | Notes |
|
||||||
| ------------------------ | :----------: | :----------: | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | :----------: | :----------: | ------------------------------------------------------- |
|
||||||
| Amcrest | ✅ | ✅ | ⛔️ Generally, Amcrest should work, but some older models (like the common IP2M-841) don't support autotracking |
|
| Amcrest | ✅ | ⛔️ | Some older models (IP2M-841) don't support autotracking |
|
||||||
| Amcrest ASH21 | ❌ | ❌ | No ONVIF support |
|
| Amcrest ASH21 | ❌ | ❌ | No ONVIF support |
|
||||||
| Ctronics PTZ | ✅ | ❌ | |
|
| Dahua | ✅ | ✅ |
|
||||||
| Dahua | ✅ | ✅ | |
|
| Reolink 511WA | ✅ | ❌ | Zoom only |
|
||||||
| Foscam R5 | ✅ | ❌ | |
|
| Reolink E1 Zoom | ✅ | ❌ | |
|
||||||
| Hikvision | ✅ | ❌ | Incomplete ONVIF support (MoveStatus won't update even on latest firmware) - reported with HWP-N4215IH-DE and DS-2DE3304W-DE, but likely others |
|
| Tapo C210 | ❌ | ❌ | Incomplete ONVIF support |
|
||||||
| Reolink 511WA | ✅ | ❌ | Zoom only |
|
| Vikylin PTZ-2804X-I2 | ❌ | ❌ | Incomplete ONVIF support |
|
||||||
| Reolink E1 Pro | ✅ | ❌ | |
|
|
||||||
| Reolink E1 Zoom | ✅ | ❌ | |
|
|
||||||
| Sunba 405-D20X | ✅ | ❌ | |
|
|
||||||
| Tapo C210 | ❌ | ❌ | Incomplete ONVIF support |
|
|
||||||
| Vikylin PTZ-2804X-I2 | ❌ | ❌ | Incomplete ONVIF support |
|
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ cameras:
|
|||||||
# Required: List of x,y coordinates to define the polygon of the zone.
|
# Required: List of x,y coordinates to define the polygon of the zone.
|
||||||
# NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box.
|
# NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box.
|
||||||
coordinates: 545,1077,747,939,788,805
|
coordinates: 545,1077,747,939,788,805
|
||||||
# Optional: Number of consecutive frames required for object to be considered present in the zone (default: shown below).
|
# Optional: Number of consecutive frames required for object to be considered present in the zone. Allowed values are 1-10 (default: shown below)
|
||||||
inertia: 3
|
inertia: 3
|
||||||
# Optional: List of objects that can trigger this zone (default: all tracked objects)
|
# Optional: List of objects that can trigger this zone (default: all tracked objects)
|
||||||
objects:
|
objects:
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ Frigate has different live view options, some of which require the bundled `go2r
|
|||||||
|
|
||||||
Live view options can be selected while viewing the live stream. The options are:
|
Live view options can be selected while viewing the live stream. The options are:
|
||||||
|
|
||||||
| Source | Latency | Frame Rate | Resolution | Audio | Requires go2rtc | Other Limitations |
|
| Source | Latency | Frame Rate | Resolution | Audio | Requires go2rtc | Other Limitations |
|
||||||
| ------ | ------- | ------------------------------------- | -------------- | ---------------------------- | --------------- | ------------------------------------------------- |
|
| ------ | ------- | ------------------------------------- | -------------- | ---------------------------- | --------------- | -------------------------------------------- |
|
||||||
| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none |
|
| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none |
|
||||||
| mse | low | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only |
|
| mse | low | native | native | yes (depends on audio codec) | yes | not supported on iOS, Firefox is h.264 only |
|
||||||
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 |
|
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 |
|
||||||
|
|
||||||
### Audio Support
|
### Audio Support
|
||||||
|
|
||||||
@@ -37,12 +37,12 @@ There may be some cameras that you would prefer to use the sub stream for live v
|
|||||||
```yaml
|
```yaml
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
test_cam:
|
rtsp_cam:
|
||||||
- rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio.
|
- rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio.
|
||||||
- "ffmpeg:test_cam#audio=opus" # <- copy of the stream which transcodes audio to opus for webrtc
|
- "ffmpeg:rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||||
test_cam_sub:
|
rtsp_cam_sub:
|
||||||
- rtsp://192.168.1.5:554/substream # <- stream which supports video & aac audio.
|
- rtsp://192.168.1.5:554/substream # <- stream which supports video & aac audio.
|
||||||
- "ffmpeg:test_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus for webrtc
|
- "ffmpeg:rtsp_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||||
|
|
||||||
cameras:
|
cameras:
|
||||||
test_cam:
|
test_cam:
|
||||||
@@ -59,7 +59,7 @@ cameras:
|
|||||||
roles:
|
roles:
|
||||||
- detect
|
- detect
|
||||||
live:
|
live:
|
||||||
stream_name: test_cam_sub
|
stream_name: rtsp_cam_sub
|
||||||
```
|
```
|
||||||
|
|
||||||
### WebRTC extra configuration:
|
### WebRTC extra configuration:
|
||||||
|
|||||||
@@ -56,27 +56,3 @@ camera:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.
|
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.
|
||||||
|
|
||||||
### Zone Inertia
|
|
||||||
|
|
||||||
Sometimes an objects bounding box may be slightly incorrect and the bottom center of the bounding box is inside the zone while the object is not actually in the zone. Zone inertia helps guard against this by requiring an object's bounding box to be within the zone for multiple consecutive frames. This value can be configured:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
camera:
|
|
||||||
zones:
|
|
||||||
front_yard:
|
|
||||||
inertia: 3
|
|
||||||
objects:
|
|
||||||
- person
|
|
||||||
```
|
|
||||||
|
|
||||||
There may also be cases where you expect an object to quickly enter and exit a zone, like when a car is pulling into the driveway, and you may want to have the object be considered present in the zone immediately:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
camera:
|
|
||||||
zones:
|
|
||||||
driveway_entrance:
|
|
||||||
inertia: 1
|
|
||||||
objects:
|
|
||||||
- car
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ cd web && npm install
|
|||||||
cd web && npm run dev
|
cd web && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 3a. Run the development server against a non-local instance
|
||||||
|
|
||||||
|
To run the development server against a non-local instance, you will need to modify the API_HOST default return in `web/src/env.js`.
|
||||||
|
|
||||||
#### 4. Making changes
|
#### 4. Making changes
|
||||||
|
|
||||||
The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
|
The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
|
||||||
|
|||||||
@@ -204,8 +204,6 @@ It is recommended to run Frigate in LXC for maximum performance. See [this discu
|
|||||||
|
|
||||||
For details on running Frigate using ESXi, please see the instructions [here](https://williamlam.com/2023/05/frigate-nvr-with-coral-tpu-igpu-passthrough-using-esxi-on-intel-nuc.html).
|
For details on running Frigate using ESXi, please see the instructions [here](https://williamlam.com/2023/05/frigate-nvr-with-coral-tpu-igpu-passthrough-using-esxi-on-intel-nuc.html).
|
||||||
|
|
||||||
If you're running Frigate on a rack mounted server and want to passthough the Google Coral, [read this.](https://github.com/blakeblackshear/frigate/issues/305)
|
|
||||||
|
|
||||||
## Synology NAS on DSM 7
|
## Synology NAS on DSM 7
|
||||||
|
|
||||||
These settings were tested on DSM 7.1.1-42962 Update 4
|
These settings were tested on DSM 7.1.1-42962 Update 4
|
||||||
|
|||||||
@@ -155,25 +155,20 @@ Version info
|
|||||||
|
|
||||||
Events from the database. Accepts the following query string parameters:
|
Events from the database. Accepts the following query string parameters:
|
||||||
|
|
||||||
| param | Type | Description |
|
| param | Type | Description |
|
||||||
| -------------------- | ----- | ----------------------------------------------------- |
|
| -------------------- | ---- | ----------------------------------------------- |
|
||||||
| `before` | int | Epoch time |
|
| `before` | int | Epoch time |
|
||||||
| `after` | int | Epoch time |
|
| `after` | int | Epoch time |
|
||||||
| `cameras` | str | , separated list of cameras |
|
| `cameras` | str | , separated list of cameras |
|
||||||
| `labels` | str | , separated list of labels |
|
| `labels` | str | , separated list of labels |
|
||||||
| `zones` | str | , separated list of zones |
|
| `zones` | str | , separated list of zones |
|
||||||
| `limit` | int | Limit the number of events returned |
|
| `limit` | int | Limit the number of events returned |
|
||||||
| `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) |
|
| `in_progress` | int | Limit to events in progress (0 or 1) |
|
||||||
| `time_range` | str | Time range in format after,before (00:00,24:00) |
|
| `time_range` | str | Time range in format after,before (00:00,24:00) |
|
||||||
| `timezone` | str | Timezone to use for time range |
|
| `timezone` | str | Timezone to use for time range |
|
||||||
| `min_score` | float | Minimum score of the event |
|
|
||||||
| `max_score` | float | Maximum score of the event |
|
|
||||||
| `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) |
|
|
||||||
| `min_length` | float | Minimum length of the event |
|
|
||||||
| `max_length` | float | Maximum length of the event |
|
|
||||||
|
|
||||||
### `GET /api/timeline`
|
### `GET /api/timeline`
|
||||||
|
|
||||||
@@ -322,12 +317,6 @@ Get PTZ info for the camera.
|
|||||||
|
|
||||||
Create a manual event with a given `label` (ex: doorbell press) to capture a specific event besides an object being detected.
|
Create a manual event with a given `label` (ex: doorbell press) to capture a specific event besides an object being detected.
|
||||||
|
|
||||||
:::caution
|
|
||||||
|
|
||||||
Recording retention config still applies to manual events, if frigate is configured with `mode: motion` then the manual event will only keep recording segments when motion occurred.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
**Optional Body:**
|
**Optional Body:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ objects:
|
|||||||
fedex:
|
fedex:
|
||||||
min_score: .75
|
min_score: .75
|
||||||
person:
|
person:
|
||||||
min_score: .65
|
min_score: .8
|
||||||
threshold: .85
|
threshold: .85
|
||||||
car:
|
car:
|
||||||
min_score: .65
|
min_score: .8
|
||||||
threshold: .85
|
threshold: .85
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -163,8 +163,6 @@ class FrigateApp:
|
|||||||
"frame_queue": mp.Queue(maxsize=2),
|
"frame_queue": mp.Queue(maxsize=2),
|
||||||
"capture_process": None,
|
"capture_process": None,
|
||||||
"process": None,
|
"process": None,
|
||||||
"audio_rms": mp.Value("d", 0.0), # type: ignore[typeddict-item]
|
|
||||||
"audio_dBFS": mp.Value("d", 0.0), # type: ignore[typeddict-item]
|
|
||||||
}
|
}
|
||||||
self.ptz_metrics[camera_name] = {
|
self.ptz_metrics[camera_name] = {
|
||||||
"ptz_autotracker_enabled": mp.Value( # type: ignore[typeddict-item]
|
"ptz_autotracker_enabled": mp.Value( # type: ignore[typeddict-item]
|
||||||
@@ -502,7 +500,6 @@ class FrigateApp:
|
|||||||
args=(
|
args=(
|
||||||
self.config,
|
self.config,
|
||||||
self.audio_recordings_info_queue,
|
self.audio_recordings_info_queue,
|
||||||
self.camera_metrics,
|
|
||||||
self.feature_metrics,
|
self.feature_metrics,
|
||||||
self.inter_process_communicator,
|
self.inter_process_communicator,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import requests
|
|||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
from frigate.comms.inter_process import InterProcessCommunicator
|
from frigate.comms.inter_process import InterProcessCommunicator
|
||||||
from frigate.config import CameraConfig, CameraInput, FfmpegConfig, FrigateConfig
|
from frigate.config import CameraConfig, FrigateConfig
|
||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
AUDIO_DURATION,
|
AUDIO_DURATION,
|
||||||
AUDIO_FORMAT,
|
AUDIO_FORMAT,
|
||||||
@@ -26,7 +26,7 @@ from frigate.const import (
|
|||||||
from frigate.ffmpeg_presets import parse_preset_input
|
from frigate.ffmpeg_presets import parse_preset_input
|
||||||
from frigate.log import LogPipe
|
from frigate.log import LogPipe
|
||||||
from frigate.object_detection import load_labels
|
from frigate.object_detection import load_labels
|
||||||
from frigate.types import CameraMetricsTypes, FeatureMetricsTypes
|
from frigate.types import FeatureMetricsTypes
|
||||||
from frigate.util.builtin import get_ffmpeg_arg_list
|
from frigate.util.builtin import get_ffmpeg_arg_list
|
||||||
from frigate.util.services import listen
|
from frigate.util.services import listen
|
||||||
from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg
|
from frigate.video import start_or_restart_ffmpeg, stop_ffmpeg
|
||||||
@@ -39,36 +39,19 @@ except ModuleNotFoundError:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_ffmpeg_command(ffmpeg: FfmpegConfig) -> list[str]:
|
def get_ffmpeg_command(input_args: list[str], input_path: str) -> list[str]:
|
||||||
ffmpeg_input: CameraInput = [i for i in ffmpeg.inputs if "audio" in i.roles][0]
|
return get_ffmpeg_arg_list(
|
||||||
input_args = get_ffmpeg_arg_list(ffmpeg.global_args) + (
|
f"ffmpeg {{}} -i {{}} -f {AUDIO_FORMAT} -ar {AUDIO_SAMPLE_RATE} -ac 1 -y {{}}".format(
|
||||||
parse_preset_input(ffmpeg_input.input_args, 1)
|
" ".join(input_args),
|
||||||
or ffmpeg_input.input_args
|
input_path,
|
||||||
or parse_preset_input(ffmpeg.input_args, 1)
|
|
||||||
or ffmpeg.input_args
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
["ffmpeg", "-vn"]
|
|
||||||
+ input_args
|
|
||||||
+ ["-i"]
|
|
||||||
+ [ffmpeg_input.path]
|
|
||||||
+ [
|
|
||||||
"-f",
|
|
||||||
f"{AUDIO_FORMAT}",
|
|
||||||
"-ar",
|
|
||||||
f"{AUDIO_SAMPLE_RATE}",
|
|
||||||
"-ac",
|
|
||||||
"1",
|
|
||||||
"-y",
|
|
||||||
"pipe:",
|
"pipe:",
|
||||||
]
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def listen_to_audio(
|
def listen_to_audio(
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
recordings_info_queue: mp.Queue,
|
recordings_info_queue: mp.Queue,
|
||||||
camera_metrics: dict[str, CameraMetricsTypes],
|
|
||||||
process_info: dict[str, FeatureMetricsTypes],
|
process_info: dict[str, FeatureMetricsTypes],
|
||||||
inter_process_communicator: InterProcessCommunicator,
|
inter_process_communicator: InterProcessCommunicator,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -97,7 +80,6 @@ def listen_to_audio(
|
|||||||
audio = AudioEventMaintainer(
|
audio = AudioEventMaintainer(
|
||||||
camera,
|
camera,
|
||||||
recordings_info_queue,
|
recordings_info_queue,
|
||||||
camera_metrics,
|
|
||||||
process_info,
|
process_info,
|
||||||
stop_event,
|
stop_event,
|
||||||
inter_process_communicator,
|
inter_process_communicator,
|
||||||
@@ -171,7 +153,6 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
self,
|
self,
|
||||||
camera: CameraConfig,
|
camera: CameraConfig,
|
||||||
recordings_info_queue: mp.Queue,
|
recordings_info_queue: mp.Queue,
|
||||||
camera_metrics: dict[str, CameraMetricsTypes],
|
|
||||||
feature_metrics: dict[str, FeatureMetricsTypes],
|
feature_metrics: dict[str, FeatureMetricsTypes],
|
||||||
stop_event: mp.Event,
|
stop_event: mp.Event,
|
||||||
inter_process_communicator: InterProcessCommunicator,
|
inter_process_communicator: InterProcessCommunicator,
|
||||||
@@ -180,16 +161,19 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
self.name = f"{camera.name}_audio_event_processor"
|
self.name = f"{camera.name}_audio_event_processor"
|
||||||
self.config = camera
|
self.config = camera
|
||||||
self.recordings_info_queue = recordings_info_queue
|
self.recordings_info_queue = recordings_info_queue
|
||||||
self.camera_metrics = camera_metrics
|
|
||||||
self.feature_metrics = feature_metrics
|
self.feature_metrics = feature_metrics
|
||||||
self.inter_process_communicator = inter_process_communicator
|
self.inter_process_communicator = inter_process_communicator
|
||||||
self.detections: dict[dict[str, any]] = {}
|
self.detections: dict[dict[str, any]] = feature_metrics
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.detector = AudioTfl(stop_event, self.config.audio.num_threads)
|
self.detector = AudioTfl(stop_event, self.config.audio.num_threads)
|
||||||
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),)
|
self.shape = (int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE)),)
|
||||||
self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2))
|
self.chunk_size = int(round(AUDIO_DURATION * AUDIO_SAMPLE_RATE * 2))
|
||||||
self.logger = logging.getLogger(f"audio.{self.config.name}")
|
self.logger = logging.getLogger(f"audio.{self.config.name}")
|
||||||
self.ffmpeg_cmd = get_ffmpeg_command(self.config.ffmpeg)
|
self.ffmpeg_cmd = get_ffmpeg_command(
|
||||||
|
get_ffmpeg_arg_list(self.config.ffmpeg.global_args)
|
||||||
|
+ parse_preset_input("preset-rtsp-audio-only", 1),
|
||||||
|
[i.path for i in self.config.ffmpeg.inputs if "audio" in i.roles][0],
|
||||||
|
)
|
||||||
self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.audio")
|
self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.audio")
|
||||||
self.audio_listener = None
|
self.audio_listener = None
|
||||||
|
|
||||||
@@ -200,9 +184,6 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
audio_as_float = audio.astype(np.float32)
|
audio_as_float = audio.astype(np.float32)
|
||||||
rms, dBFS = self.calculate_audio_levels(audio_as_float)
|
rms, dBFS = self.calculate_audio_levels(audio_as_float)
|
||||||
|
|
||||||
self.camera_metrics[self.config.name]["audio_rms"].value = rms
|
|
||||||
self.camera_metrics[self.config.name]["audio_dBFS"].value = dBFS
|
|
||||||
|
|
||||||
# only run audio detection when volume is above min_volume
|
# only run audio detection when volume is above min_volume
|
||||||
if rms >= self.config.audio.min_volume:
|
if rms >= self.config.audio.min_volume:
|
||||||
# add audio info to recordings queue
|
# add audio info to recordings queue
|
||||||
@@ -215,8 +196,6 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
model_detections = self.detector.detect(waveform)
|
model_detections = self.detector.detect(waveform)
|
||||||
|
|
||||||
for label, score, _ in model_detections:
|
for label, score, _ in model_detections:
|
||||||
logger.debug(f"Heard {label} with a score of {score}")
|
|
||||||
|
|
||||||
if label not in self.config.audio.listen:
|
if label not in self.config.audio.listen:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,6 @@ def should_update_state(prev_event: Event, current_event: Event) -> bool:
|
|||||||
if prev_event["stationary"] != current_event["stationary"]:
|
if prev_event["stationary"] != current_event["stationary"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if prev_event["attributes"] != current_event["attributes"]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -256,6 +256,13 @@ PRESETS_INPUT = {
|
|||||||
"-use_wallclock_as_timestamps",
|
"-use_wallclock_as_timestamps",
|
||||||
"1",
|
"1",
|
||||||
],
|
],
|
||||||
|
"preset-rtsp-audio-only": [
|
||||||
|
"-rtsp_transport",
|
||||||
|
"tcp",
|
||||||
|
TIMEOUT_PARAM,
|
||||||
|
"5000000",
|
||||||
|
"-vn",
|
||||||
|
],
|
||||||
"preset-rtsp-restream": _user_agent_args
|
"preset-rtsp-restream": _user_agent_args
|
||||||
+ [
|
+ [
|
||||||
"-rtsp_transport",
|
"-rtsp_transport",
|
||||||
|
|||||||
233
frigate/http.py
233
frigate/http.py
@@ -20,7 +20,6 @@ from flask import (
|
|||||||
Flask,
|
Flask,
|
||||||
Response,
|
Response,
|
||||||
current_app,
|
current_app,
|
||||||
escape,
|
|
||||||
jsonify,
|
jsonify,
|
||||||
make_response,
|
make_response,
|
||||||
request,
|
request,
|
||||||
@@ -29,7 +28,6 @@ from peewee import DoesNotExist, fn, operator
|
|||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
from playhouse.sqliteq import SqliteQueueDatabase
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
from tzlocal import get_localzone_name
|
from tzlocal import get_localzone_name
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
@@ -75,13 +73,6 @@ def create_app(
|
|||||||
):
|
):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def check_csrf():
|
|
||||||
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
|
||||||
pass
|
|
||||||
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
|
||||||
return jsonify({"success": False, "message": "Missing CSRF header"}), 401
|
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def _db_connect():
|
def _db_connect():
|
||||||
if database.is_closed():
|
if database.is_closed():
|
||||||
@@ -279,11 +270,9 @@ def send_to_plus(id):
|
|||||||
event.label,
|
event.label,
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
# log the exception, but dont return an error response
|
||||||
|
logger.warn(f"Unable to upload annotation for {event.label} to Frigate+")
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return make_response(
|
|
||||||
jsonify({"success": False, "message": str(ex)}),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
|
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
|
||||||
|
|
||||||
@@ -350,6 +339,7 @@ def false_positive(id):
|
|||||||
event.detector_type,
|
event.detector_type,
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
logger.warn(f"Unable to upload false positive for {event.label} to Frigate+")
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({"success": False, "message": str(ex)}),
|
jsonify({"success": False, "message": str(ex)}),
|
||||||
@@ -541,14 +531,10 @@ def event_thumbnail(id, max_cache_age=2592000):
|
|||||||
if tracked_obj is not None:
|
if tracked_obj is not None:
|
||||||
thumbnail_bytes = tracked_obj.get_thumbnail()
|
thumbnail_bytes = tracked_obj.get_thumbnail()
|
||||||
except Exception:
|
except Exception:
|
||||||
return make_response(
|
return "Event not found", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
if thumbnail_bytes is None:
|
if thumbnail_bytes is None:
|
||||||
return make_response(
|
return "Event not found", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
# android notifications prefer a 2:1 ratio
|
# android notifications prefer a 2:1 ratio
|
||||||
if format == "android":
|
if format == "android":
|
||||||
@@ -643,9 +629,7 @@ def event_snapshot(id):
|
|||||||
event = Event.get(Event.id == id, Event.end_time != None)
|
event = Event.get(Event.id == id, Event.end_time != None)
|
||||||
event_complete = True
|
event_complete = True
|
||||||
if not event.has_snapshot:
|
if not event.has_snapshot:
|
||||||
return make_response(
|
return "Snapshot not available", 404
|
||||||
jsonify({"success": False, "message": "Snapshot not available"}), 404
|
|
||||||
)
|
|
||||||
# read snapshot from disk
|
# read snapshot from disk
|
||||||
with open(
|
with open(
|
||||||
os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), "rb"
|
os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), "rb"
|
||||||
@@ -667,18 +651,12 @@ def event_snapshot(id):
|
|||||||
quality=request.args.get("quality", default=70, type=int),
|
quality=request.args.get("quality", default=70, type=int),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return make_response(
|
return "Event not found", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return make_response(
|
return "Event not found", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
if jpg_bytes is None:
|
if jpg_bytes is None:
|
||||||
return make_response(
|
return "Event not found", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
response = make_response(jpg_bytes)
|
response = make_response(jpg_bytes)
|
||||||
response.headers["Content-Type"] = "image/jpeg"
|
response.headers["Content-Type"] = "image/jpeg"
|
||||||
@@ -731,14 +709,10 @@ def event_clip(id):
|
|||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == id)
|
event: Event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return "Event not found.", 404
|
||||||
jsonify({"success": False, "message": "Event not found"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
if not event.has_clip:
|
if not event.has_clip:
|
||||||
return make_response(
|
return "Clip not available", 404
|
||||||
jsonify({"success": False, "message": "Clip not available"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
file_name = f"{event.camera}-{id}.mp4"
|
file_name = f"{event.camera}-{id}.mp4"
|
||||||
clip_path = os.path.join(CLIPS_DIR, file_name)
|
clip_path = os.path.join(CLIPS_DIR, file_name)
|
||||||
@@ -802,11 +776,6 @@ def events():
|
|||||||
in_progress = request.args.get("in_progress", 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)
|
||||||
min_score = request.args.get("min_score", type=float)
|
|
||||||
max_score = request.args.get("max_score", type=float)
|
|
||||||
is_submitted = request.args.get("is_submitted", type=int)
|
|
||||||
min_length = request.args.get("min_length", type=float)
|
|
||||||
max_length = request.args.get("max_length", type=float)
|
|
||||||
|
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
@@ -929,24 +898,6 @@ def events():
|
|||||||
if favorites:
|
if favorites:
|
||||||
clauses.append((Event.retain_indefinitely == favorites))
|
clauses.append((Event.retain_indefinitely == favorites))
|
||||||
|
|
||||||
if max_score is not None:
|
|
||||||
clauses.append((Event.data["score"] <= max_score))
|
|
||||||
|
|
||||||
if min_score is not None:
|
|
||||||
clauses.append((Event.data["score"] >= min_score))
|
|
||||||
|
|
||||||
if min_length is not None:
|
|
||||||
clauses.append(((Event.end_time - Event.start_time) >= min_length))
|
|
||||||
|
|
||||||
if max_length is not None:
|
|
||||||
clauses.append(((Event.end_time - Event.start_time) <= max_length))
|
|
||||||
|
|
||||||
if is_submitted is not None:
|
|
||||||
if is_submitted == 0:
|
|
||||||
clauses.append((Event.plus_id.is_null()))
|
|
||||||
else:
|
|
||||||
clauses.append((Event.plus_id != ""))
|
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append((True))
|
||||||
|
|
||||||
@@ -1067,9 +1018,7 @@ def config_raw():
|
|||||||
config_file = config_file_yaml
|
config_file = config_file_yaml
|
||||||
|
|
||||||
if not os.path.isfile(config_file):
|
if not os.path.isfile(config_file):
|
||||||
return make_response(
|
return "Could not find file", 410
|
||||||
jsonify({"success": False, "message": "Could not find file"}), 404
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(config_file, "r") as f:
|
with open(config_file, "r") as f:
|
||||||
raw_config = f.read()
|
raw_config = f.read()
|
||||||
@@ -1085,12 +1034,7 @@ def config_save():
|
|||||||
new_config = request.get_data().decode()
|
new_config = request.get_data().decode()
|
||||||
|
|
||||||
if not new_config:
|
if not new_config:
|
||||||
return make_response(
|
return "Config with body param is required", 400
|
||||||
jsonify(
|
|
||||||
{"success": False, "message": "Config with body param is required"}
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate the config schema
|
# Validate the config schema
|
||||||
try:
|
try:
|
||||||
@@ -1100,7 +1044,7 @@ def config_save():
|
|||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"\nConfig Error:\n\n{escape(str(traceback.format_exc()))}",
|
"message": f"\nConfig Error:\n\n{str(traceback.format_exc())}",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
400,
|
400,
|
||||||
@@ -1135,30 +1079,14 @@ def config_save():
|
|||||||
restart_frigate()
|
restart_frigate()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error restarting Frigate: {e}")
|
logging.error(f"Error restarting Frigate: {e}")
|
||||||
return make_response(
|
return "Config successfully saved, unable to restart Frigate", 200
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Config successfully saved, unable to restart Frigate",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
return make_response(
|
return (
|
||||||
jsonify(
|
"Config successfully saved, restarting (this can take up to one minute)...",
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Config successfully saved, restarting (this can take up to one minute)...",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
200,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return "Config successfully saved.", 200
|
||||||
jsonify({"success": True, "message": "Config successfully saved."}),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/config/set", methods=["PUT"])
|
@bp.route("/config/set", methods=["PUT"])
|
||||||
@@ -1198,20 +1126,9 @@ def config_set():
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error updating config: {e}")
|
logging.error(f"Error updating config: {e}")
|
||||||
return make_response(
|
return "Error updating config", 500
|
||||||
jsonify({"success": False, "message": "Error updating config"}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
return make_response(
|
return "Config successfully updated, restart to apply", 200
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Config successfully updated, restart to apply",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/config/schema.json")
|
@bp.route("/config/schema.json")
|
||||||
@@ -1261,10 +1178,7 @@ def mjpeg_feed(camera_name):
|
|||||||
mimetype="multipart/x-mixed-replace; boundary=frame",
|
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return "Camera named {} not found".format(camera_name), 404
|
||||||
jsonify({"success": False, "message": "Camera not found"}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<camera_name>/ptz/info")
|
@bp.route("/<camera_name>/ptz/info")
|
||||||
@@ -1272,10 +1186,7 @@ def camera_ptz_info(camera_name):
|
|||||||
if camera_name in current_app.frigate_config.cameras:
|
if camera_name in current_app.frigate_config.cameras:
|
||||||
return jsonify(current_app.onvif.get_camera_info(camera_name))
|
return jsonify(current_app.onvif.get_camera_info(camera_name))
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return "Camera named {} not found".format(camera_name), 404
|
||||||
jsonify({"success": False, "message": "Camera not found"}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<camera_name>/latest.jpg")
|
@bp.route("/<camera_name>/latest.jpg")
|
||||||
@@ -1317,10 +1228,7 @@ def latest_frame(camera_name):
|
|||||||
width = int(height * frame.shape[1] / frame.shape[0])
|
width = int(height * frame.shape[1] / frame.shape[0])
|
||||||
|
|
||||||
if frame is None:
|
if frame is None:
|
||||||
return make_response(
|
return "Unable to get valid frame from {}".format(camera_name), 500
|
||||||
jsonify({"success": False, "message": "Unable to get valid frame"}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|
||||||
if height < 1 or width < 1:
|
if height < 1 or width < 1:
|
||||||
return (
|
return (
|
||||||
@@ -1356,10 +1264,7 @@ def latest_frame(camera_name):
|
|||||||
response.headers["Cache-Control"] = "no-store"
|
response.headers["Cache-Control"] = "no-store"
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return make_response(
|
return "Camera named {} not found".format(camera_name), 404
|
||||||
jsonify({"success": False, "message": "Camera not found"}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<camera_name>/recordings/<frame_time>/snapshot.png")
|
@bp.route("/<camera_name>/recordings/<frame_time>/snapshot.png")
|
||||||
@@ -1374,10 +1279,7 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(
|
((frame_time > Recordings.start_time) & (frame_time < Recordings.end_time))
|
||||||
(frame_time >= Recordings.start_time)
|
|
||||||
& (frame_time <= Recordings.end_time)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.where(Recordings.camera == camera_name)
|
.where(Recordings.camera == camera_name)
|
||||||
)
|
)
|
||||||
@@ -1412,15 +1314,7 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
|||||||
response.headers["Content-Type"] = "image/png"
|
response.headers["Content-Type"] = "image/png"
|
||||||
return response
|
return response
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return "Recording not found for {} at {}".format(camera_name, frame_time), 404
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "Recording not found at {}".format(frame_time),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/recordings/storage", methods=["GET"])
|
@bp.route("/recordings/storage", methods=["GET"])
|
||||||
@@ -1622,15 +1516,7 @@ def recording_clip(camera_name, start_ts, end_ts):
|
|||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
logger.error(p.stderr)
|
logger.error(p.stderr)
|
||||||
return make_response(
|
return f"Could not create clip from recordings for {camera_name}.", 500
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "Could not create clip from recordings",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Ignoring subsequent request for {path} as it already exists in the cache."
|
f"Ignoring subsequent request for {path} as it already exists in the cache."
|
||||||
@@ -1686,15 +1572,7 @@ def vod_ts(camera_name, start_ts, end_ts):
|
|||||||
|
|
||||||
if not clips:
|
if not clips:
|
||||||
logger.error("No recordings found for the requested time range")
|
logger.error("No recordings found for the requested time range")
|
||||||
return make_response(
|
return "No recordings found.", 404
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "No recordings found.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
hour_ago = datetime.now() - timedelta(hours=1)
|
hour_ago = datetime.now() - timedelta(hours=1)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -1737,27 +1615,11 @@ def vod_event(id):
|
|||||||
event: Event = Event.get(Event.id == id)
|
event: Event = Event.get(Event.id == id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
logger.error(f"Event not found: {id}")
|
logger.error(f"Event not found: {id}")
|
||||||
return make_response(
|
return "Event not found.", 404
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "Event not found.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not event.has_clip:
|
if not event.has_clip:
|
||||||
logger.error(f"Event does not have recordings: {id}")
|
logger.error(f"Event does not have recordings: {id}")
|
||||||
return make_response(
|
return "Recordings not available", 404
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": "Recordings not available.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
|
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
|
||||||
|
|
||||||
@@ -1834,21 +1696,12 @@ def export_recording(camera_name: str, start_time, end_time):
|
|||||||
else PlaybackFactorEnum.realtime,
|
else PlaybackFactorEnum.realtime,
|
||||||
)
|
)
|
||||||
exporter.start()
|
exporter.start()
|
||||||
return make_response(
|
return "Starting export of recording", 200
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Starting export of recording.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/export/<file_name>", methods=["DELETE"])
|
@bp.route("/export/<file_name>", methods=["DELETE"])
|
||||||
def export_delete(file_name: str):
|
def export_delete(file_name: str):
|
||||||
safe_file_name = secure_filename(file_name)
|
file = os.path.join(EXPORT_DIR, file_name)
|
||||||
file = os.path.join(EXPORT_DIR, safe_file_name)
|
|
||||||
|
|
||||||
if not os.path.exists(file):
|
if not os.path.exists(file):
|
||||||
return make_response(
|
return make_response(
|
||||||
@@ -1857,15 +1710,7 @@ def export_delete(file_name: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
os.unlink(file)
|
os.unlink(file)
|
||||||
return make_response(
|
return "Successfully deleted file", 200
|
||||||
jsonify(
|
|
||||||
{
|
|
||||||
"success": True,
|
|
||||||
"message": "Successfully deleted file.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
|
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
|
||||||
@@ -1965,11 +1810,8 @@ def logs(service: str):
|
|||||||
}
|
}
|
||||||
service_location = log_locations.get(service)
|
service_location = log_locations.get(service)
|
||||||
|
|
||||||
if not service_location:
|
if not service:
|
||||||
return make_response(
|
return f"{service} is not a valid service", 404
|
||||||
jsonify({"success": False, "message": "Not a valid service"}),
|
|
||||||
404,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file = open(service_location, "r")
|
file = open(service_location, "r")
|
||||||
@@ -1977,7 +1819,4 @@ def logs(service: str):
|
|||||||
file.close()
|
file.close()
|
||||||
return contents, 200
|
return contents, 200
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
return make_response(
|
return f"Could not find log file: {e}", 500
|
||||||
jsonify({"success": False, "message": f"Could not find log file: {e}"}),
|
|
||||||
500,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -20,7 +20,3 @@ class MotionDetector(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def detect(self, frame):
|
def detect(self, frame):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_calibrating(self):
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ class FrigateMotionDetector(MotionDetector):
|
|||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
self.contour_area = contour_area
|
self.contour_area = contour_area
|
||||||
|
|
||||||
def is_calibrating(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def detect(self, frame):
|
def detect(self, frame):
|
||||||
motion_boxes = []
|
motion_boxes = []
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ class ImprovedMotionDetector(MotionDetector):
|
|||||||
self.contrast_values[:, 1:2] = 255
|
self.contrast_values[:, 1:2] = 255
|
||||||
self.contrast_values_index = 0
|
self.contrast_values_index = 0
|
||||||
|
|
||||||
def is_calibrating(self):
|
|
||||||
return self.calibrating
|
|
||||||
|
|
||||||
def detect(self, frame):
|
def detect(self, frame):
|
||||||
motion_boxes = []
|
motion_boxes = []
|
||||||
|
|
||||||
@@ -144,6 +141,7 @@ class ImprovedMotionDetector(MotionDetector):
|
|||||||
|
|
||||||
# if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate
|
# if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate
|
||||||
if self.calibrating or pct_motion > self.config.lightning_threshold:
|
if self.calibrating or pct_motion > self.config.lightning_threshold:
|
||||||
|
motion_boxes = []
|
||||||
self.calibrating = True
|
self.calibrating = True
|
||||||
|
|
||||||
if self.save_images:
|
if self.save_images:
|
||||||
|
|||||||
@@ -232,9 +232,6 @@ class TrackedObject:
|
|||||||
if self.obj_data["position_changes"] != obj_data["position_changes"]:
|
if self.obj_data["position_changes"] != obj_data["position_changes"]:
|
||||||
significant_change = True
|
significant_change = True
|
||||||
|
|
||||||
if self.obj_data["attributes"] != obj_data["attributes"]:
|
|
||||||
significant_change = True
|
|
||||||
|
|
||||||
# if the motionless_count reaches the stationary threshold
|
# if the motionless_count reaches the stationary threshold
|
||||||
if (
|
if (
|
||||||
self.obj_data["motionless_count"]
|
self.obj_data["motionless_count"]
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def get_canvas_shape(width: int, height: int) -> tuple[int, int]:
|
|||||||
|
|
||||||
if round(a_w / a_h, 2) != round(width / height, 2):
|
if round(a_w / a_h, 2) != round(width / height, 2):
|
||||||
canvas_width = width
|
canvas_width = width
|
||||||
canvas_height = int((canvas_width / a_w) * a_h)
|
canvas_height = (canvas_width / a_w) * a_h
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
|
f"The birdseye resolution is a non-standard aspect ratio, forcing birdseye resolution to {canvas_width} x {canvas_height}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,12 +237,13 @@ class PtzAutoTracker:
|
|||||||
self.onvif.get_camera_status(camera_name)
|
self.onvif.get_camera_status(camera_name)
|
||||||
|
|
||||||
# movement thread per camera
|
# movement thread per camera
|
||||||
self.move_threads[camera_name] = threading.Thread(
|
if not self.move_threads or not self.move_threads[camera_name]:
|
||||||
name=f"move_thread_{camera_name}",
|
self.move_threads[camera_name] = threading.Thread(
|
||||||
target=partial(self._process_move_queue, camera_name),
|
name=f"move_thread_{camera_name}",
|
||||||
)
|
target=partial(self._process_move_queue, camera_name),
|
||||||
self.move_threads[camera_name].daemon = True
|
)
|
||||||
self.move_threads[camera_name].start()
|
self.move_threads[camera_name].daemon = True
|
||||||
|
self.move_threads[camera_name].start()
|
||||||
|
|
||||||
if cam.onvif.autotracking.movement_weights:
|
if cam.onvif.autotracking.movement_weights:
|
||||||
self.intercept[camera_name] = cam.onvif.autotracking.movement_weights[0]
|
self.intercept[camera_name] = cam.onvif.autotracking.movement_weights[0]
|
||||||
|
|||||||
@@ -131,24 +131,18 @@ class OnvifController:
|
|||||||
|
|
||||||
# try setting relative zoom translation space
|
# try setting relative zoom translation space
|
||||||
try:
|
try:
|
||||||
if (
|
if self.config.cameras[camera_name].onvif.autotracking.zooming:
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
if zoom_space_id is not None:
|
if zoom_space_id is not None:
|
||||||
move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
||||||
"RelativeZoomTranslationSpace"
|
"RelativeZoomTranslationSpace"
|
||||||
][0]["URI"]
|
][0]["URI"]
|
||||||
except Exception:
|
except Exception:
|
||||||
if (
|
if self.config.cameras[camera_name].onvif.autotracking.zoom_relative:
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
self.config.cameras[
|
self.config.cameras[
|
||||||
camera_name
|
camera_name
|
||||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
].onvif.autotracking.zoom_relative = False
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported"
|
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported"
|
||||||
)
|
)
|
||||||
|
|
||||||
if move_request.Speed is None:
|
if move_request.Speed is None:
|
||||||
@@ -175,9 +169,7 @@ class OnvifController:
|
|||||||
presets = []
|
presets = []
|
||||||
|
|
||||||
for preset in presets:
|
for preset in presets:
|
||||||
self.cams[camera_name]["presets"][
|
self.cams[camera_name]["presets"][preset["Name"].lower()] = preset["token"]
|
||||||
getattr(preset, "Name", f"preset {preset['token']}").lower()
|
|
||||||
] = preset["token"]
|
|
||||||
|
|
||||||
# get list of supported features
|
# get list of supported features
|
||||||
ptz_config = ptz.GetConfigurationOptions(request)
|
ptz_config = ptz.GetConfigurationOptions(request)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class RecordingExporter(threading.Thread):
|
|||||||
|
|
||||||
def get_datetime_from_timestamp(self, timestamp: int) -> str:
|
def get_datetime_from_timestamp(self, timestamp: int) -> str:
|
||||||
"""Convenience fun to get a simple date time from timestamp."""
|
"""Convenience fun to get a simple date time from timestamp."""
|
||||||
return datetime.datetime.fromtimestamp(timestamp).strftime("%Y_%m_%d_%H_%M")
|
return datetime.datetime.fromtimestamp(timestamp).strftime("%Y_%m_%d_%H:%M")
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -355,7 +355,6 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
"+faststart",
|
"+faststart",
|
||||||
file_path,
|
file_path,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
await p.wait()
|
await p.wait()
|
||||||
|
|
||||||
|
|||||||
@@ -176,8 +176,6 @@ async def set_gpu_stats(
|
|||||||
stats[nvidia_usage[i]["name"]] = {
|
stats[nvidia_usage[i]["name"]] = {
|
||||||
"gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%",
|
"gpu": str(round(float(nvidia_usage[i]["gpu"]), 2)) + "%",
|
||||||
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
|
"mem": str(round(float(nvidia_usage[i]["mem"]), 2)) + "%",
|
||||||
"enc": str(round(float(nvidia_usage[i]["enc"]), 2)) + "%",
|
|
||||||
"dec": str(round(float(nvidia_usage[i]["dec"]), 2)) + "%",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -268,8 +266,6 @@ def stats_snapshot(
|
|||||||
"pid": pid,
|
"pid": pid,
|
||||||
"capture_pid": cpid,
|
"capture_pid": cpid,
|
||||||
"ffmpeg_pid": ffmpeg_pid,
|
"ffmpeg_pid": ffmpeg_pid,
|
||||||
"audio_rms": round(camera_stats["audio_rms"].value, 4),
|
|
||||||
"audio_dBFS": round(camera_stats["audio_dBFS"].value, 4),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stats["detectors"] = {}
|
stats["detectors"] = {}
|
||||||
|
|||||||
@@ -1027,12 +1027,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
"roles": ["detect"],
|
"roles": ["detect"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"detect": {
|
|
||||||
"height": 720,
|
|
||||||
"width": 1280,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1087,11 +1082,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"snapshots": {
|
"snapshots": {
|
||||||
"height": 100,
|
"height": 100,
|
||||||
},
|
},
|
||||||
@@ -1117,12 +1107,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
"roles": ["detect"],
|
"roles": ["detect"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1147,11 +1132,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"snapshots": {
|
"snapshots": {
|
||||||
"height": 150,
|
"height": 150,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -1180,11 +1160,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1206,12 +1181,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
"roles": ["detect"],
|
"roles": ["detect"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1235,11 +1205,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"rtmp": {
|
"rtmp": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
},
|
},
|
||||||
@@ -1269,11 +1234,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1297,11 +1257,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1323,12 +1278,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
"roles": ["detect"],
|
"roles": ["detect"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1352,11 +1302,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"live": {
|
"live": {
|
||||||
"quality": 7,
|
"quality": 7,
|
||||||
},
|
},
|
||||||
@@ -1384,11 +1329,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1410,12 +1350,7 @@ class TestConfig(unittest.TestCase):
|
|||||||
"roles": ["detect"],
|
"roles": ["detect"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1440,11 +1375,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"timestamp_style": {"position": "bl", "thickness": 4},
|
"timestamp_style": {"position": "bl", "thickness": 4},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1470,11 +1400,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1498,11 +1423,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1530,11 +1450,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1560,11 +1475,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"zones": {
|
"zones": {
|
||||||
"steps": {
|
"steps": {
|
||||||
"coordinates": "0,0,0,0",
|
"coordinates": "0,0,0,0",
|
||||||
@@ -1636,11 +1546,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"onvif": {"autotracking": {"movement_weights": "1.23, 2.34, 0.50"}},
|
"onvif": {"autotracking": {"movement_weights": "1.23, 2.34, 0.50"}},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1664,11 +1569,6 @@ class TestConfig(unittest.TestCase):
|
|||||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"detect": {
|
|
||||||
"height": 1080,
|
|
||||||
"width": 1920,
|
|
||||||
"fps": 5,
|
|
||||||
},
|
|
||||||
"onvif": {"autotracking": {"movement_weights": "1.234, 2.345a"}},
|
"onvif": {"autotracking": {"movement_weights": "1.234, 2.345a"}},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class TimelineProcessor(threading.Thread):
|
|||||||
camera_config.detect.height,
|
camera_config.detect.height,
|
||||||
event_data["region"],
|
event_data["region"],
|
||||||
),
|
),
|
||||||
"attribute": "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if event_type == "start":
|
if event_type == "start":
|
||||||
@@ -94,12 +93,6 @@ class TimelineProcessor(threading.Thread):
|
|||||||
"stationary" if event_data["stationary"] else "active"
|
"stationary" if event_data["stationary"] else "active"
|
||||||
)
|
)
|
||||||
Timeline.insert(timeline_entry).execute()
|
Timeline.insert(timeline_entry).execute()
|
||||||
elif prev_event_data["attributes"] == {} and event_data["attributes"] != {}:
|
|
||||||
timeline_entry[Timeline.class_type] = "attribute"
|
|
||||||
timeline_entry[Timeline.data]["attribute"] = list(
|
|
||||||
event_data["attributes"].keys()
|
|
||||||
)[0]
|
|
||||||
Timeline.insert(timeline_entry).execute()
|
|
||||||
elif event_type == "end":
|
elif event_type == "end":
|
||||||
timeline_entry[Timeline.class_type] = "gone"
|
timeline_entry[Timeline.class_type] = "gone"
|
||||||
Timeline.insert(timeline_entry).execute()
|
Timeline.insert(timeline_entry).execute()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class NorfairTracker(ObjectTracker):
|
|||||||
self.tracker = Tracker(
|
self.tracker = Tracker(
|
||||||
distance_function=frigate_distance,
|
distance_function=frigate_distance,
|
||||||
distance_threshold=2.5,
|
distance_threshold=2.5,
|
||||||
initialization_delay=config.detect.fps / 2,
|
initialization_delay=0,
|
||||||
hit_counter_max=self.max_disappeared,
|
hit_counter_max=self.max_disappeared,
|
||||||
)
|
)
|
||||||
if self.ptz_autotracker_enabled.value:
|
if self.ptz_autotracker_enabled.value:
|
||||||
@@ -106,6 +106,11 @@ class NorfairTracker(ObjectTracker):
|
|||||||
"ymax": self.detect_config.height,
|
"ymax": self.detect_config.height,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# start object with a hit count of `fps` to avoid quick detection -> loss
|
||||||
|
next(
|
||||||
|
(o for o in self.tracker.tracked_objects if o.global_id == track_id)
|
||||||
|
).hit_counter = self.camera_config.detect.fps
|
||||||
|
|
||||||
def deregister(self, id, track_id):
|
def deregister(self, id, track_id):
|
||||||
del self.tracked_objects[id]
|
del self.tracked_objects[id]
|
||||||
del self.disappeared[id]
|
del self.disappeared[id]
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ class CameraMetricsTypes(TypedDict):
|
|||||||
process_fps: Synchronized
|
process_fps: Synchronized
|
||||||
read_start: Synchronized
|
read_start: Synchronized
|
||||||
skipped_fps: Synchronized
|
skipped_fps: Synchronized
|
||||||
audio_rms: Synchronized
|
|
||||||
audio_dBFS: Synchronized
|
|
||||||
|
|
||||||
|
|
||||||
class PTZMetricsTypes(TypedDict):
|
class PTZMetricsTypes(TypedDict):
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ def load_config_with_no_duplicates(raw_config) -> dict:
|
|||||||
"""Get config ensuring duplicate keys are not allowed."""
|
"""Get config ensuring duplicate keys are not allowed."""
|
||||||
|
|
||||||
# https://stackoverflow.com/a/71751051
|
# https://stackoverflow.com/a/71751051
|
||||||
# important to use SafeLoader here to avoid RCE
|
class PreserveDuplicatesLoader(yaml.loader.Loader):
|
||||||
class PreserveDuplicatesLoader(yaml.loader.SafeLoader):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def map_constructor(loader, node, deep=False):
|
def map_constructor(loader, node, deep=False):
|
||||||
|
|||||||
@@ -293,8 +293,6 @@ def get_nvidia_gpu_stats() -> dict[int, dict]:
|
|||||||
handle = nvml.nvmlDeviceGetHandleByIndex(i)
|
handle = nvml.nvmlDeviceGetHandleByIndex(i)
|
||||||
meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle)
|
meminfo = try_get_info(nvml.nvmlDeviceGetMemoryInfo, handle)
|
||||||
util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle)
|
util = try_get_info(nvml.nvmlDeviceGetUtilizationRates, handle)
|
||||||
enc = try_get_info(nvml.nvmlDeviceGetEncoderUtilization, handle)
|
|
||||||
dec = try_get_info(nvml.nvmlDeviceGetDecoderUtilization, handle)
|
|
||||||
if util != "N/A":
|
if util != "N/A":
|
||||||
gpu_util = util.gpu
|
gpu_util = util.gpu
|
||||||
else:
|
else:
|
||||||
@@ -305,22 +303,10 @@ def get_nvidia_gpu_stats() -> dict[int, dict]:
|
|||||||
else:
|
else:
|
||||||
gpu_mem_util = -1
|
gpu_mem_util = -1
|
||||||
|
|
||||||
if enc != "N/A":
|
|
||||||
enc_util = enc[0]
|
|
||||||
else:
|
|
||||||
enc_util = -1
|
|
||||||
|
|
||||||
if dec != "N/A":
|
|
||||||
dec_util = dec[0]
|
|
||||||
else:
|
|
||||||
dec_util = -1
|
|
||||||
|
|
||||||
results[i] = {
|
results[i] = {
|
||||||
"name": nvml.nvmlDeviceGetName(handle),
|
"name": nvml.nvmlDeviceGetName(handle),
|
||||||
"gpu": gpu_util,
|
"gpu": gpu_util,
|
||||||
"mem": gpu_mem_util,
|
"mem": gpu_mem_util,
|
||||||
"enc": enc_util,
|
|
||||||
"dec": dec_util,
|
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from frigate.log import LogPipe
|
|||||||
from frigate.motion import MotionDetector
|
from frigate.motion import MotionDetector
|
||||||
from frigate.motion.improved_motion import ImprovedMotionDetector
|
from frigate.motion.improved_motion import ImprovedMotionDetector
|
||||||
from frigate.object_detection import RemoteObjectDetector
|
from frigate.object_detection import RemoteObjectDetector
|
||||||
|
from frigate.ptz.autotrack import ptz_moving_at_frame_time
|
||||||
from frigate.track import ObjectTracker
|
from frigate.track import ObjectTracker
|
||||||
from frigate.track.norfair_tracker import NorfairTracker
|
from frigate.track.norfair_tracker import NorfairTracker
|
||||||
from frigate.types import PTZMetricsTypes
|
from frigate.types import PTZMetricsTypes
|
||||||
@@ -776,8 +777,19 @@ def process_frames(
|
|||||||
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
|
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# look for motion if enabled
|
# look for motion if enabled and ptz is not moving
|
||||||
motion_boxes = motion_detector.detect(frame) if motion_enabled.value else []
|
# ptz_moving_at_frame_time() always returns False for
|
||||||
|
# non ptz/autotracking cameras
|
||||||
|
motion_boxes = (
|
||||||
|
motion_detector.detect(frame)
|
||||||
|
if motion_enabled.value
|
||||||
|
and not ptz_moving_at_frame_time(
|
||||||
|
frame_time,
|
||||||
|
ptz_metrics["ptz_start_time"].value,
|
||||||
|
ptz_metrics["ptz_stop_time"].value,
|
||||||
|
)
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
regions = []
|
regions = []
|
||||||
consolidated_detections = []
|
consolidated_detections = []
|
||||||
@@ -802,10 +814,8 @@ def process_frames(
|
|||||||
)
|
)
|
||||||
# and it hasn't disappeared
|
# and it hasn't disappeared
|
||||||
and object_tracker.disappeared[obj["id"]] == 0
|
and object_tracker.disappeared[obj["id"]] == 0
|
||||||
# and it doesn't overlap with any current motion boxes when not calibrating
|
# and it doesn't overlap with any current motion boxes
|
||||||
and not intersects_any(
|
and not intersects_any(obj["box"], motion_boxes)
|
||||||
obj["box"], [] if motion_detector.is_calibrating() else motion_boxes
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# get tracked object boxes that aren't stationary
|
# get tracked object boxes that aren't stationary
|
||||||
@@ -815,10 +825,7 @@ def process_frames(
|
|||||||
if obj["id"] not in stationary_object_ids
|
if obj["id"] not in stationary_object_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
combined_boxes = tracked_object_boxes
|
combined_boxes = motion_boxes + tracked_object_boxes
|
||||||
# only add in the motion boxes when not calibrating
|
|
||||||
if not motion_detector.is_calibrating():
|
|
||||||
combined_boxes += motion_boxes
|
|
||||||
|
|
||||||
cluster_candidates = get_cluster_candidates(
|
cluster_candidates = get_cluster_candidates(
|
||||||
frame_shape, region_min_size, combined_boxes
|
frame_shape, region_min_size, combined_boxes
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"ignorePatterns": ["*.d.ts"],
|
"ignorePatterns": ["*.d.ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
"comma-dangle": [
|
"comma-dangle": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"trailingComma": "es5",
|
|
||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
// import { API_HOST } from '../src/env';
|
import { API_HOST } from '../src/env';
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
rest.get(`api/config`, (req, res, ctx) => {
|
rest.get(`${API_HOST}api/config`, (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
ctx.json({
|
ctx.json({
|
||||||
@@ -37,7 +37,7 @@ export const handlers = [
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
rest.get(`api/stats`, (req, res, ctx) => {
|
rest.get(`${API_HOST}api/stats`, (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
ctx.json({
|
ctx.json({
|
||||||
@@ -58,7 +58,7 @@ export const handlers = [
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
rest.get(`api/events`, (req, res, ctx) => {
|
rest.get(`${API_HOST}api/events`, (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
ctx.json(
|
ctx.json(
|
||||||
@@ -77,7 +77,7 @@ export const handlers = [
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
rest.get(`api/sub_labels`, (req, res, ctx) => {
|
rest.get(`${API_HOST}api/sub_labels`, (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
ctx.json([
|
ctx.json([
|
||||||
|
|||||||
1290
web/package-lock.json
generated
1290
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,6 @@
|
|||||||
"eslint-config-preact": "^1.3.0",
|
"eslint-config-preact": "^1.3.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-jest": "^27.2.3",
|
"eslint-plugin-jest": "^27.2.3",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
|
||||||
"eslint-plugin-vitest-globals": "^1.4.0",
|
"eslint-plugin-vitest-globals": "^1.4.0",
|
||||||
"fake-indexeddb": "^4.0.1",
|
"fake-indexeddb": "^4.0.1",
|
||||||
"jsdom": "^22.0.0",
|
"jsdom": "^22.0.0",
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const ENV = 'test';
|
export const ENV = 'test';
|
||||||
|
export const API_HOST = 'http://base-url.local:5000/';
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ describe('useApiHost', () => {
|
|||||||
<Test />
|
<Test />
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
);
|
);
|
||||||
expect(screen.queryByText('http://localhost:3000/')).toBeInTheDocument();
|
expect(screen.queryByText('http://base-url.local:5000/')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;
|
import { API_HOST } from '../env';
|
||||||
|
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { WsProvider } from './ws';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||||
axios.defaults.headers.common = {
|
|
||||||
'X-CSRF-TOKEN': 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ApiProvider({ children, options }) {
|
export function ApiProvider({ children, options }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch
|
|||||||
if (!config || scaledHeight === 0 || !canvasRef.current) {
|
if (!config || scaledHeight === 0 || !canvasRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
|
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
|
||||||
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
|
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ export const HistoryVideo = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
video.src({
|
video.src({
|
||||||
src: `${apiHost}vod/event/${id}/master.m3u8`,
|
src: `${apiHost}/vod/event/${id}/master.m3u8`,
|
||||||
type: 'application/vnd.apple.mpegurl',
|
type: 'application/vnd.apple.mpegurl',
|
||||||
});
|
});
|
||||||
video.poster(`${apiHost}api/events/${id}/snapshot.jpg`);
|
video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`);
|
||||||
if (videoIsPlaying) {
|
if (videoIsPlaying) {
|
||||||
video.play();
|
video.play();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,9 +157,12 @@ class VideoRTC extends HTMLElement {
|
|||||||
if (this.ws) this.ws.send(JSON.stringify(value));
|
if (this.ws) this.ws.send(JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Function} isSupported */
|
codecs(type) {
|
||||||
codecs(isSupported) {
|
const test =
|
||||||
return this.CODECS.filter(codec => isSupported(`video/mp4; codecs="${codec}"`)).join();
|
type === 'mse'
|
||||||
|
? (codec) => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
|
||||||
|
: (codec) => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
|
||||||
|
return this.CODECS.filter(test).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,7 +311,7 @@ class VideoRTC extends HTMLElement {
|
|||||||
|
|
||||||
const modes = [];
|
const modes = [];
|
||||||
|
|
||||||
if (this.mode.indexOf('mse') >= 0 && ('MediaSource' in window || 'ManagedMediaSource' in window)) {
|
if (this.mode.indexOf('mse') >= 0 && 'MediaSource' in window) {
|
||||||
// iPhone
|
// iPhone
|
||||||
modes.push('mse');
|
modes.push('mse');
|
||||||
this.onmse();
|
this.onmse();
|
||||||
@@ -360,29 +363,18 @@ class VideoRTC extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onmse() {
|
onmse() {
|
||||||
/** @type {MediaSource} */
|
const ms = new MediaSource();
|
||||||
let ms;
|
ms.addEventListener(
|
||||||
|
'sourceopen',
|
||||||
if ('ManagedMediaSource' in window) {
|
() => {
|
||||||
const MediaSource = window.ManagedMediaSource;
|
|
||||||
|
|
||||||
ms = new MediaSource();
|
|
||||||
ms.addEventListener('sourceopen', () => {
|
|
||||||
this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
this.video.disableRemotePlayback = true;
|
|
||||||
this.video.srcObject = ms;
|
|
||||||
} else {
|
|
||||||
ms = new MediaSource();
|
|
||||||
ms.addEventListener('sourceopen', () => {
|
|
||||||
URL.revokeObjectURL(this.video.src);
|
URL.revokeObjectURL(this.video.src);
|
||||||
this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});
|
this.send({ type: 'mse', value: this.codecs('mse') });
|
||||||
}, {once: true});
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
this.video.src = URL.createObjectURL(ms);
|
this.video.src = URL.createObjectURL(ms);
|
||||||
this.video.srcObject = null;
|
this.video.srcObject = null;
|
||||||
}
|
|
||||||
this.play();
|
this.play();
|
||||||
|
|
||||||
this.mseCodecs = '';
|
this.mseCodecs = '';
|
||||||
@@ -588,7 +580,7 @@ class VideoRTC extends HTMLElement {
|
|||||||
video2.src = `data:video/mp4;base64,${VideoRTC.btoa(data)}`;
|
video2.src = `data:video/mp4;base64,${VideoRTC.btoa(data)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.send({ type: 'mp4', value: this.codecs(this.video.canPlayType) });
|
this.send({ type: 'mp4', value: this.codecs('mp4') });
|
||||||
}
|
}
|
||||||
|
|
||||||
static btoa(buffer) {
|
static btoa(buffer) {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import Menu from './Menu';
|
|||||||
import { ArrowDropdown } from '../icons/ArrowDropdown';
|
import { ArrowDropdown } from '../icons/ArrowDropdown';
|
||||||
import Heading from './Heading';
|
import Heading from './Heading';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import SelectOnlyIcon from '../icons/SelectOnly';
|
import CameraIcon from '../icons/Camera';
|
||||||
|
import SpeakerIcon from '../icons/Speaker';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
|
||||||
const popupRef = useRef(null);
|
const popupRef = useRef(null);
|
||||||
@@ -18,6 +20,7 @@ export default function MultiSelect({ className, title, options, selection, onTo
|
|||||||
};
|
};
|
||||||
|
|
||||||
const menuHeight = Math.round(window.innerHeight * 0.55);
|
const menuHeight = Math.round(window.innerHeight * 0.55);
|
||||||
|
const { data: config } = useSWR('config');
|
||||||
return (
|
return (
|
||||||
<div className={`${className} p-2`} ref={popupRef}>
|
<div className={`${className} p-2`} ref={popupRef}>
|
||||||
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
|
||||||
@@ -58,7 +61,8 @@ export default function MultiSelect({ className, title, options, selection, onTo
|
|||||||
className="max-h-[35px] mx-2"
|
className="max-h-[35px] mx-2"
|
||||||
onClick={() => onSelectSingle(item)}
|
onClick={() => onSelectSingle(item)}
|
||||||
>
|
>
|
||||||
{ ( <SelectOnlyIcon /> ) }
|
{ (title === "Labels" && config.audio.listen.includes(item)) ? ( <SpeakerIcon /> ) : ( <CameraIcon /> ) }
|
||||||
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export function EventCard({ camera, event }) {
|
|||||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
|
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
|
||||||
<div className="flex flex-row mb-2">
|
<div className="flex flex-row mb-2">
|
||||||
<div className="w-28 mr-4">
|
<div className="w-28 mr-4">
|
||||||
<img className="antialiased" loading="lazy" src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} />
|
<img className="antialiased" loading="lazy" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row w-full border-b">
|
<div className="flex flex-row w-full border-b">
|
||||||
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useCallback, useState } from 'preact/hooks';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Switch({ className, checked, id, onChange, label, labelPosition = 'before' }) {
|
export default function Switch({ checked, id, onChange, label, labelPosition = 'before' }) {
|
||||||
const [isFocused, setFocused] = useState(false);
|
const [isFocused, setFocused] = useState(false);
|
||||||
|
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
@@ -21,7 +21,7 @@ export default function Switch({ className, checked, id, onChange, label, labelP
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={`${className ? className : ''} flex items-center space-x-4 w-full ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`}
|
className={`flex items-center space-x-4 w-full ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`}
|
||||||
>
|
>
|
||||||
{label && labelPosition === 'before' ? (
|
{label && labelPosition === 'before' ? (
|
||||||
<div data-testid={`${id}-label`} className="inline-flex flex-grow">
|
<div data-testid={`${id}-label`} className="inline-flex flex-grow">
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import ActiveObjectIcon from '../icons/ActiveObject';
|
|||||||
import PlayIcon from '../icons/Play';
|
import PlayIcon from '../icons/Play';
|
||||||
import ExitIcon from '../icons/Exit';
|
import ExitIcon from '../icons/Exit';
|
||||||
import StationaryObjectIcon from '../icons/StationaryObject';
|
import StationaryObjectIcon from '../icons/StationaryObject';
|
||||||
import FaceIcon from '../icons/Face';
|
import { Zone } from '../icons/Zone';
|
||||||
import LicensePlateIcon from '../icons/LicensePlate';
|
|
||||||
import DeliveryTruckIcon from '../icons/DeliveryTruck';
|
|
||||||
import ZoneIcon from '../icons/Zone';
|
|
||||||
import { useMemo, useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
|
||||||
@@ -91,7 +88,7 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
aria-label={window.innerWidth > 640 ? getTimelineItemDescription(config, item, event) : ''}
|
||||||
onClick={() => onSelectMoment(index)}
|
onClick={() => onSelectMoment(index)}
|
||||||
>
|
>
|
||||||
{getTimelineIcon(item)}
|
{getTimelineIcon(item.class_type)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -116,8 +113,8 @@ export default function TimelineSummary({ event, onFrameSelected }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimelineIcon(timelineItem) {
|
function getTimelineIcon(classType) {
|
||||||
switch (timelineItem.class_type) {
|
switch (classType) {
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return <PlayIcon className="w-8" />;
|
return <PlayIcon className="w-8" />;
|
||||||
case 'gone':
|
case 'gone':
|
||||||
@@ -127,16 +124,7 @@ function getTimelineIcon(timelineItem) {
|
|||||||
case 'stationary':
|
case 'stationary':
|
||||||
return <StationaryObjectIcon className="w-8" />;
|
return <StationaryObjectIcon className="w-8" />;
|
||||||
case 'entered_zone':
|
case 'entered_zone':
|
||||||
return <ZoneIcon className="w-8" />;
|
return <Zone className="w-8" />;
|
||||||
case 'attribute':
|
|
||||||
switch (timelineItem.data.attribute) {
|
|
||||||
case 'face':
|
|
||||||
return <FaceIcon className="w-8" />;
|
|
||||||
case 'license_plate':
|
|
||||||
return <LicensePlateIcon className="w-8" />;
|
|
||||||
default:
|
|
||||||
return <DeliveryTruckIcon className="w-8" />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,15 +156,6 @@ function getTimelineItemDescription(config, timelineItem, event) {
|
|||||||
time_style: 'medium',
|
time_style: 'medium',
|
||||||
time_format: config.ui.time_format,
|
time_format: config.ui.time_format,
|
||||||
})}`;
|
})}`;
|
||||||
case 'attribute':
|
|
||||||
return `${timelineItem.data.attribute.replaceAll("_", " ")} detected for ${event.label} at ${formatUnixTimestampToDateTime(
|
|
||||||
timelineItem.timestamp,
|
|
||||||
{
|
|
||||||
date_style: 'short',
|
|
||||||
time_style: 'medium',
|
|
||||||
time_format: config.ui.time_format,
|
|
||||||
}
|
|
||||||
)}`;
|
|
||||||
case 'gone':
|
case 'gone':
|
||||||
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
return `${event.label} left at ${formatUnixTimestampToDateTime(timelineItem.timestamp, {
|
||||||
date_style: 'short',
|
date_style: 'short',
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const ENV = import.meta.env.MODE;
|
export const ENV = import.meta.env.MODE;
|
||||||
|
export const API_HOST = ENV === 'production' ? '' : 'http://localhost:5000/';
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { memo } from 'preact/compat';
|
|
||||||
|
|
||||||
export function StationaryObject({ className = '' }) {
|
|
||||||
return (
|
|
||||||
<svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M240-160q-50 0-85-35t-35-85H40v-440q0-33 23.5-56.5T120-800h560v160h120l120 160v200h-80q0 50-35 85t-85 35q-50 0-85-35t-35-85H360q0 50-35 85t-85 35Zm0-80q17 0 28.5-11.5T280-280q0-17-11.5-28.5T240-320q-17 0-28.5 11.5T200-280q0 17 11.5 28.5T240-240ZM120-360h32q17-18 39-29t49-11q27 0 49 11t39 29h272v-360H120v360Zm600 120q17 0 28.5-11.5T760-280q0-17-11.5-28.5T720-320q-17 0-28.5 11.5T680-280q0 17 11.5 28.5T720-240Zm-40-200h170l-90-120h-80v120ZM360-540Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(StationaryObject);
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { memo } from 'preact/compat';
|
|
||||||
|
|
||||||
export function Zone({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={className}
|
|
||||||
fill={fill}
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
stroke={stroke}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M360-390q-21 0-35.5-14.5T310-440q0-21 14.5-35.5T360-490q21 0 35.5 14.5T410-440q0 21-14.5 35.5T360-390Zm240 0q-21 0-35.5-14.5T550-440q0-21 14.5-35.5T600-490q21 0 35.5 14.5T650-440q0 21-14.5 35.5T600-390ZM480-160q134 0 227-93t93-227q0-24-3-46.5T786-570q-21 5-42 7.5t-44 2.5q-91 0-172-39T390-708q-32 78-91.5 135.5T160-486v6q0 134 93 227t227 93Zm0 80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm-54-715q42 70 114 112.5T700-640q14 0 27-1.5t27-3.5q-42-70-114-112.5T480-800q-14 0-27 1.5t-27 3.5ZM177-581q51-29 89-75t57-103q-51 29-89 75t-57 103Zm249-214Zm-103 36Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(Zone);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { memo } from 'preact/compat';
|
|
||||||
|
|
||||||
export function StationaryObject({ className = '' }) {
|
|
||||||
return (
|
|
||||||
<svg className={`fill-current ${className}`} viewBox="0 -960 960 960">
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M400-280h360v-240H400v240ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(StationaryObject);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { memo } from 'preact/compat';
|
|
||||||
|
|
||||||
export function SelectOnly({ className = 'h-5 w-5', stroke = 'currentColor', fill = 'none', onClick = () => {} }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={className}
|
|
||||||
fill={fill}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke={stroke}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-7 7H3v4c0 1.1.9 2 2 2h4v-2H5v-4zM5 5h4V3H5c-1.1 0-2 .9-2 2v4h2V5zm14-2h-4v2h4v4h2V5c0-1.1-.9-2-2-2zm0 16h-4v2h4c1.1 0 2-.9 2-2v-4h-2v4z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(SelectOnly);
|
|
||||||
@@ -35,7 +35,7 @@ export default function Birdseye() {
|
|||||||
let player;
|
let player;
|
||||||
const playerClass = ptzCameras.length || isMaxWidth ? 'w-full' : 'max-w-5xl xl:w-1/2';
|
const playerClass = ptzCameras.length || isMaxWidth ? 'w-full' : 'max-w-5xl xl:w-1/2';
|
||||||
if (viewSource == 'mse' && config.birdseye.restream) {
|
if (viewSource == 'mse' && config.birdseye.restream) {
|
||||||
if ('MediaSource' in window || 'ManagedMediaSource' in window) {
|
if ('MediaSource' in window) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={playerClass}>
|
<div className={playerClass}>
|
||||||
@@ -50,7 +50,7 @@ export default function Birdseye() {
|
|||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="w-5xl text-center text-sm">
|
<div className="w-5xl text-center text-sm">
|
||||||
MSE is only supported on iOS 17.1+. You'll need to update if available or use jsmpeg / webRTC streams. See the docs for more info.
|
MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info.
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function Camera({ camera }) {
|
|||||||
let player;
|
let player;
|
||||||
if (viewMode === 'live') {
|
if (viewMode === 'live') {
|
||||||
if (viewSource == 'mse' && restreamEnabled) {
|
if (viewSource == 'mse' && restreamEnabled) {
|
||||||
if ('MediaSource' in window || 'ManagedMediaSource' in window) {
|
if ('MediaSource' in window) {
|
||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="max-w-5xl">
|
<div className="max-w-5xl">
|
||||||
@@ -133,7 +133,7 @@ export default function Camera({ camera }) {
|
|||||||
player = (
|
player = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="w-5xl text-center text-sm">
|
<div className="w-5xl text-center text-sm">
|
||||||
MSE is only supported on iOS 17.1+. You'll need to update if available or use jsmpeg / webRTC streams. See the docs for more info.
|
MSE is not supported on iOS devices. You'll need to use jsmpeg or webRTC. See the docs for more info.
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -212,7 +212,7 @@ export default function Camera({ camera }) {
|
|||||||
key={objectType}
|
key={objectType}
|
||||||
header={objectType}
|
header={objectType}
|
||||||
href={`/events?cameras=${camera}&labels=${encodeURIComponent(objectType)}`}
|
href={`/events?cameras=${camera}&labels=${encodeURIComponent(objectType)}`}
|
||||||
media={<img src={`${apiHost}api/${camera}/${encodeURIComponent(objectType)}/thumbnail.jpg`} />}
|
media={<img src={`${apiHost}/api/${camera}/${encodeURIComponent(objectType)}/thumbnail.jpg`} />}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export default function CameraMasks({ camera }) {
|
|||||||
Array.isArray(motionMask)
|
Array.isArray(motionMask)
|
||||||
? motionMask.map((mask) => getPolylinePoints(mask))
|
? motionMask.map((mask) => getPolylinePoints(mask))
|
||||||
: motionMask
|
: motionMask
|
||||||
? [getPolylinePoints(motionMask)]
|
? [getPolylinePoints(motionMask)]
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
const [zonePoints, setZonePoints] = useState(
|
const [zonePoints, setZonePoints] = useState(
|
||||||
@@ -45,8 +45,8 @@ export default function CameraMasks({ camera }) {
|
|||||||
[name]: Array.isArray(objectFilters[name].mask)
|
[name]: Array.isArray(objectFilters[name].mask)
|
||||||
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
|
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
|
||||||
: objectFilters[name].mask
|
: objectFilters[name].mask
|
||||||
? [getPolylinePoints(objectFilters[name].mask)]
|
? [getPolylinePoints(objectFilters[name].mask)]
|
||||||
: [],
|
: [],
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
@@ -135,7 +135,7 @@ export default function CameraMasks({ camera }) {
|
|||||||
const endpoint = `config/set?${queryParameters}`;
|
const endpoint = `config/set?${queryParameters}`;
|
||||||
const response = await axios.put(endpoint);
|
const response = await axios.put(endpoint);
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setSuccess(response.data.message);
|
setSuccess(response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -146,6 +146,7 @@ export default function CameraMasks({ camera }) {
|
|||||||
}
|
}
|
||||||
}, [camera, motionMaskPoints]);
|
}, [camera, motionMaskPoints]);
|
||||||
|
|
||||||
|
|
||||||
// Zone methods
|
// Zone methods
|
||||||
const handleEditZone = useCallback(
|
const handleEditZone = useCallback(
|
||||||
(key) => {
|
(key) => {
|
||||||
@@ -174,11 +175,9 @@ export default function CameraMasks({ camera }) {
|
|||||||
const handleCopyZones = useCallback(async () => {
|
const handleCopyZones = useCallback(async () => {
|
||||||
const textToCopy = ` zones:
|
const textToCopy = ` zones:
|
||||||
${Object.keys(zonePoints)
|
${Object.keys(zonePoints)
|
||||||
.map(
|
.map(
|
||||||
(zoneName) => ` ${zoneName}:
|
(zoneName) => ` ${zoneName}:
|
||||||
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
|
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`).join('\n')}`;
|
||||||
)
|
|
||||||
.join('\n')}`;
|
|
||||||
|
|
||||||
if (window.navigator.clipboard && window.navigator.clipboard.writeText) {
|
if (window.navigator.clipboard && window.navigator.clipboard.writeText) {
|
||||||
// Use Clipboard API if available
|
// Use Clipboard API if available
|
||||||
@@ -208,10 +207,7 @@ ${Object.keys(zonePoints)
|
|||||||
const handleSaveZones = useCallback(async () => {
|
const handleSaveZones = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const queryParameters = Object.keys(zonePoints)
|
const queryParameters = Object.keys(zonePoints)
|
||||||
.map(
|
.map((zoneName) => `cameras.${camera}.zones.${zoneName}.coordinates=${polylinePointsToPolyline(zonePoints[zoneName])}`)
|
||||||
(zoneName) =>
|
|
||||||
`cameras.${camera}.zones.${zoneName}.coordinates=${polylinePointsToPolyline(zonePoints[zoneName])}`
|
|
||||||
)
|
|
||||||
.join('&');
|
.join('&');
|
||||||
const endpoint = `config/set?${queryParameters}`;
|
const endpoint = `config/set?${queryParameters}`;
|
||||||
const response = await axios.put(endpoint);
|
const response = await axios.put(endpoint);
|
||||||
@@ -256,26 +252,21 @@ ${Object.keys(zonePoints)
|
|||||||
await window.navigator.clipboard.writeText(` objects:
|
await window.navigator.clipboard.writeText(` objects:
|
||||||
filters:
|
filters:
|
||||||
${Object.keys(objectMaskPoints)
|
${Object.keys(objectMaskPoints)
|
||||||
.map((objectName) =>
|
.map((objectName) =>
|
||||||
objectMaskPoints[objectName].length
|
objectMaskPoints[objectName].length
|
||||||
? ` ${objectName}:
|
? ` ${objectName}:
|
||||||
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
|
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')}`);
|
.join('\n')}`);
|
||||||
}, [objectMaskPoints]);
|
}, [objectMaskPoints]);
|
||||||
|
|
||||||
const handleSaveObjectMasks = useCallback(async () => {
|
const handleSaveObjectMasks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const queryParameters = Object.keys(objectMaskPoints)
|
const queryParameters = Object.keys(objectMaskPoints)
|
||||||
.filter((objectName) => objectMaskPoints[objectName].length > 0)
|
.filter((objectName) => objectMaskPoints[objectName].length > 0)
|
||||||
.map(
|
.map((objectName, index) => `cameras.${camera}.objects.filters.${objectName}.mask.${index}=${polylinePointsToPolyline(objectMaskPoints[objectName])}`)
|
||||||
(objectName, index) =>
|
|
||||||
`cameras.${camera}.objects.filters.${objectName}.mask.${index}=${polylinePointsToPolyline(
|
|
||||||
objectMaskPoints[objectName]
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
.join('&');
|
.join('&');
|
||||||
const endpoint = `config/set?${queryParameters}`;
|
const endpoint = `config/set?${queryParameters}`;
|
||||||
const response = await axios.put(endpoint);
|
const response = await axios.put(endpoint);
|
||||||
@@ -333,8 +324,8 @@ ${Object.keys(objectMaskPoints)
|
|||||||
<Card
|
<Card
|
||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
When done, copy each mask configuration into your <code className="font-mono">config.yml</code> file restart
|
When done, copy each mask configuration into your <code className="font-mono">config.yml</code> file
|
||||||
your Frigate instance to save your changes.
|
restart your Frigate instance to save your changes.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
header="Warning"
|
header="Warning"
|
||||||
@@ -345,7 +336,7 @@ ${Object.keys(objectMaskPoints)
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img ref={imageRef} src={`${apiHost}api/${camera}/latest.jpg`} />
|
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||||
<EditableMask
|
<EditableMask
|
||||||
onChange={handleUpdateEditable}
|
onChange={handleUpdateEditable}
|
||||||
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
||||||
@@ -496,16 +487,16 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
|||||||
{!scaledPoints
|
{!scaledPoints
|
||||||
? null
|
? null
|
||||||
: scaledPoints.map(([x, y], i) => (
|
: scaledPoints.map(([x, y], i) => (
|
||||||
<PolyPoint
|
<PolyPoint
|
||||||
key={i}
|
key={i}
|
||||||
boundingRef={boundingRef}
|
boundingRef={boundingRef}
|
||||||
index={i}
|
index={i}
|
||||||
onMove={handleMovePoint}
|
onMove={handleMovePoint}
|
||||||
onRemove={handleRemovePoint}
|
onRemove={handleRemovePoint}
|
||||||
x={x + MaskInset}
|
x={x + MaskInset}
|
||||||
y={y + MaskInset}
|
y={y + MaskInset}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
|
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
|
||||||
<svg
|
<svg
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -578,6 +569,8 @@ function MaskValues({
|
|||||||
[onAdd]
|
[onAdd]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
|
<div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
|
|||||||
@@ -5,41 +5,24 @@ import CameraImage from '../components/CameraImage';
|
|||||||
import AudioIcon from '../icons/Audio';
|
import AudioIcon from '../icons/Audio';
|
||||||
import ClipIcon from '../icons/Clip';
|
import ClipIcon from '../icons/Clip';
|
||||||
import MotionIcon from '../icons/Motion';
|
import MotionIcon from '../icons/Motion';
|
||||||
import SettingsIcon from '../icons/Settings';
|
|
||||||
import SnapshotIcon from '../icons/Snapshot';
|
import SnapshotIcon from '../icons/Snapshot';
|
||||||
import { useAudioState, useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws';
|
import { useAudioState, useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws';
|
||||||
import { useMemo } from 'preact/hooks';
|
import { useMemo } from 'preact/hooks';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { useResizeObserver } from '../hooks';
|
|
||||||
import Dialog from '../components/Dialog';
|
|
||||||
import Switch from '../components/Switch';
|
|
||||||
import Heading from '../components/Heading';
|
|
||||||
import Button from '../components/Button';
|
|
||||||
|
|
||||||
export default function Cameras() {
|
export default function Cameras() {
|
||||||
const { data: config } = useSWR('config');
|
const { data: config } = useSWR('config');
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
const [{ width: containerWidth }] = useResizeObserver(containerRef);
|
|
||||||
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
|
|
||||||
// https://github.com/blakeblackshear/frigate/issues/1657
|
|
||||||
let scrollBarWidth = 0;
|
|
||||||
if (window.innerWidth && document.body.offsetWidth) {
|
|
||||||
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
|
|
||||||
}
|
|
||||||
const availableWidth = scrollBarWidth ? containerWidth + scrollBarWidth : containerWidth;
|
|
||||||
|
|
||||||
return !config ? (
|
return !config ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4 p-2 px-4" ref={containerRef}>
|
<div className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4 p-2 px-4">
|
||||||
<SortedCameras config={config} unsortedCameras={config.cameras} availableWidth={availableWidth} />
|
<SortedCameras config={config} unsortedCameras={config.cameras} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortedCameras({ config, unsortedCameras, availableWidth }) {
|
function SortedCameras({ config, unsortedCameras }) {
|
||||||
const sortedCameras = useMemo(
|
const sortedCameras = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.entries(unsortedCameras)
|
Object.entries(unsortedCameras)
|
||||||
@@ -51,20 +34,17 @@ function SortedCameras({ config, unsortedCameras, availableWidth }) {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{sortedCameras.map(([camera, conf]) => (
|
{sortedCameras.map(([camera, conf]) => (
|
||||||
<Camera key={camera} name={camera} config={config.cameras[camera]} conf={conf} availableWidth={availableWidth} />
|
<Camera key={camera} name={camera} config={config.cameras[camera]} conf={conf} />
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Camera({ name, config, availableWidth }) {
|
function Camera({ name, config }) {
|
||||||
const { payload: detectValue, send: sendDetect } = useDetectState(name);
|
const { payload: detectValue, send: sendDetect } = useDetectState(name);
|
||||||
const { payload: recordValue, send: sendRecordings } = useRecordingsState(name);
|
const { payload: recordValue, send: sendRecordings } = useRecordingsState(name);
|
||||||
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
|
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
|
||||||
const { payload: audioValue, send: sendAudio } = useAudioState(name);
|
const { payload: audioValue, send: sendAudio } = useAudioState(name);
|
||||||
|
|
||||||
const [cameraOptions, setCameraOptions] = useState('');
|
|
||||||
|
|
||||||
const href = `/cameras/${name}`;
|
const href = `/cameras/${name}`;
|
||||||
const buttons = useMemo(() => {
|
const buttons = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@@ -76,15 +56,7 @@ function Camera({ name, config, availableWidth }) {
|
|||||||
return `${name.replaceAll('_', ' ')}`;
|
return `${name.replaceAll('_', ' ')}`;
|
||||||
}, [name]);
|
}, [name]);
|
||||||
const icons = useMemo(
|
const icons = useMemo(
|
||||||
() => (availableWidth < 448 ? [
|
() => [
|
||||||
{
|
|
||||||
icon: SettingsIcon,
|
|
||||||
color: 'gray',
|
|
||||||
onClick: () => {
|
|
||||||
setCameraOptions(config.name);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] : [
|
|
||||||
{
|
{
|
||||||
name: `Toggle detect ${detectValue === 'ON' ? 'off' : 'on'}`,
|
name: `Toggle detect ${detectValue === 'ON' ? 'off' : 'on'}`,
|
||||||
icon: MotionIcon,
|
icon: MotionIcon,
|
||||||
@@ -123,64 +95,17 @@ function Camera({ name, config, availableWidth }) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
]).filter((button) => button != null),
|
].filter((button) => button != null),
|
||||||
[config, availableWidth, setCameraOptions, audioValue, sendAudio, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
|
[config, audioValue, sendAudio, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Card
|
||||||
{cameraOptions && (
|
buttons={buttons}
|
||||||
<Dialog>
|
href={href}
|
||||||
<div className="p-4">
|
header={cleanName}
|
||||||
<Heading size="md">{`${name.replaceAll('_', ' ')} Settings`}</Heading>
|
icons={icons}
|
||||||
<Switch
|
media={<CameraImage camera={name} stretch />}
|
||||||
className="my-3"
|
/>
|
||||||
checked={detectValue == 'ON'}
|
|
||||||
id="detect"
|
|
||||||
onChange={() => sendDetect(detectValue === 'ON' ? 'OFF' : 'ON', true)}
|
|
||||||
label="Detect"
|
|
||||||
labelPosition="before"
|
|
||||||
/>
|
|
||||||
{config.record.enabled_in_config && <Switch
|
|
||||||
className="my-3"
|
|
||||||
checked={recordValue == 'ON'}
|
|
||||||
id="record"
|
|
||||||
onChange={() => sendRecordings(recordValue === 'ON' ? 'OFF' : 'ON', true)}
|
|
||||||
label="Recordings"
|
|
||||||
labelPosition="before"
|
|
||||||
/>}
|
|
||||||
<Switch
|
|
||||||
className="my-3"
|
|
||||||
checked={snapshotValue == 'ON'}
|
|
||||||
id="snapshot"
|
|
||||||
onChange={() => sendSnapshots(snapshotValue === 'ON' ? 'OFF' : 'ON', true)}
|
|
||||||
label="Snapshots"
|
|
||||||
labelPosition="before"
|
|
||||||
/>
|
|
||||||
{config.audio.enabled_in_config && <Switch
|
|
||||||
className="my-3"
|
|
||||||
checked={audioValue == 'ON'}
|
|
||||||
id="audio"
|
|
||||||
onChange={() => sendAudio(audioValue === 'ON' ? 'OFF' : 'ON', true)}
|
|
||||||
label="Audio Detection"
|
|
||||||
labelPosition="before"
|
|
||||||
/>}
|
|
||||||
</div>
|
|
||||||
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
|
||||||
<Button className="ml-2" onClick={() => setCameraOptions('')} type="text">
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card
|
|
||||||
buttons={buttons}
|
|
||||||
href={href}
|
|
||||||
header={cleanName}
|
|
||||||
icons={icons}
|
|
||||||
media={<CameraImage camera={name} stretch />}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ export default function Config() {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess(response.data.message);
|
setSuccess(response.data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
setError(error.response.data.message);
|
setError(error.response.data.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -61,9 +61,9 @@ export default function Config() {
|
|||||||
|
|
||||||
let yamlModel;
|
let yamlModel;
|
||||||
if (editor.getModels().length > 0) {
|
if (editor.getModels().length > 0) {
|
||||||
yamlModel = editor.getModel(modelUri);
|
yamlModel = editor.getModel(modelUri)
|
||||||
} else {
|
} else {
|
||||||
yamlModel = editor.createModel(config, 'yaml', modelUri);
|
yamlModel = editor.createModel(config, 'yaml', modelUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDiagnosticsOptions({
|
setDiagnosticsOptions({
|
||||||
@@ -74,7 +74,7 @@ export default function Config() {
|
|||||||
format: true,
|
format: true,
|
||||||
schemas: [
|
schemas: [
|
||||||
{
|
{
|
||||||
uri: `${apiHost}api/config/schema.json`,
|
uri: `${apiHost}/api/config/schema.json`,
|
||||||
fileMatch: [String(modelUri)],
|
fileMatch: [String(modelUri)],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -100,10 +100,10 @@ export default function Config() {
|
|||||||
<Button className="mx-2" onClick={(e) => handleCopyConfig(e)}>
|
<Button className="mx-2" onClick={(e) => handleCopyConfig(e)}>
|
||||||
Copy Config
|
Copy Config
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e, 'restart')}>
|
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e, "restart")}>
|
||||||
Save & Restart
|
Save & Restart
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e, 'saveonly')}>
|
<Button className="mx-2" onClick={(e) => onHandleSaveConfig(e, "saveonly")}>
|
||||||
Save Only
|
Save Only
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -402,7 +402,7 @@ export default function Events({ path, ...props }) {
|
|||||||
icon={Snapshot}
|
icon={Snapshot}
|
||||||
label="Download Snapshot"
|
label="Download Snapshot"
|
||||||
value="snapshot"
|
value="snapshot"
|
||||||
href={`${apiHost}api/events/${downloadEvent.id}/snapshot.jpg?download=true`}
|
href={`${apiHost}/api/events/${downloadEvent.id}/snapshot.jpg?download=true`}
|
||||||
download
|
download
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -411,7 +411,7 @@ export default function Events({ path, ...props }) {
|
|||||||
icon={Clip}
|
icon={Clip}
|
||||||
label="Download Clip"
|
label="Download Clip"
|
||||||
value="clip"
|
value="clip"
|
||||||
href={`${apiHost}api/events/${downloadEvent.id}/clip.mp4?download=true`}
|
href={`${apiHost}/api/events/${downloadEvent.id}/clip.mp4?download=true`}
|
||||||
download
|
download
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -419,13 +419,13 @@ export default function Events({ path, ...props }) {
|
|||||||
downloadEvent.end_time &&
|
downloadEvent.end_time &&
|
||||||
downloadEvent.has_snapshot &&
|
downloadEvent.has_snapshot &&
|
||||||
!downloadEvent.plus_id && (
|
!downloadEvent.plus_id && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={UploadPlus}
|
icon={UploadPlus}
|
||||||
label={uploading.includes(downloadEvent.id) ? 'Uploading...' : 'Send to Frigate+'}
|
label={uploading.includes(downloadEvent.id) ? 'Uploading...' : 'Send to Frigate+'}
|
||||||
value="plus"
|
value="plus"
|
||||||
onSelect={() => showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)}
|
onSelect={() => showSubmitToPlus(downloadEvent.id, downloadEvent.label, downloadEvent.box)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{downloadEvent.plus_id && (
|
{downloadEvent.plus_id && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={UploadPlus}
|
icon={UploadPlus}
|
||||||
@@ -492,7 +492,7 @@ export default function Events({ path, ...props }) {
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
className="flex-grow-0"
|
className="flex-grow-0"
|
||||||
src={`${apiHost}api/events/${plusSubmitEvent.id}/snapshot.jpg`}
|
src={`${apiHost}/api/events/${plusSubmitEvent.id}/snapshot.jpg`}
|
||||||
alt={`${plusSubmitEvent.label}`}
|
alt={`${plusSubmitEvent.label}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -619,7 +619,7 @@ export default function Events({ path, ...props }) {
|
|||||||
<div
|
<div
|
||||||
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center"
|
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center"
|
||||||
style={{
|
style={{
|
||||||
'background-image': `url(${apiHost}api/events/${event.id}/thumbnail.jpg)`,
|
'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StarRecording
|
<StarRecording
|
||||||
@@ -776,8 +776,8 @@ export default function Events({ path, ...props }) {
|
|||||||
className="flex-grow-0"
|
className="flex-grow-0"
|
||||||
src={
|
src={
|
||||||
event.has_snapshot
|
event.has_snapshot
|
||||||
? `${apiHost}api/events/${event.id}/snapshot.jpg`
|
? `${apiHost}/api/events/${event.id}/snapshot.jpg`
|
||||||
: `${apiHost}api/events/${event.id}/thumbnail.jpg`
|
: `${apiHost}/api/events/${event.id}/thumbnail.jpg`
|
||||||
}
|
}
|
||||||
alt={`${event.label} at ${((event?.data?.top_score || event.top_score) * 100).toFixed(
|
alt={`${event.label} at ${((event?.data?.top_score || event.top_score) * 100).toFixed(
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ export default function Storage() {
|
|||||||
const getUnitSize = (MB) => {
|
const getUnitSize = (MB) => {
|
||||||
if (isNaN(MB) || MB < 0) return 'Invalid number';
|
if (isNaN(MB) || MB < 0) return 'Invalid number';
|
||||||
if (MB < 1024) return `${MB} MiB`;
|
if (MB < 1024) return `${MB} MiB`;
|
||||||
if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`;
|
|
||||||
|
|
||||||
return `${(MB / 1048576).toFixed(2)} TiB`;
|
return `${(MB / 1024).toFixed(2)} GiB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
let storage_usage;
|
let storage_usage;
|
||||||
|
|||||||
@@ -301,16 +301,12 @@ export default function System() {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>GPU %</Th>
|
<Th>GPU %</Th>
|
||||||
<Th>Memory %</Th>
|
<Th>Memory %</Th>
|
||||||
{'dec' in gpu_usages[gpu] && (<Th>Decoder %</Th>)}
|
|
||||||
{'enc' in gpu_usages[gpu] && (<Th>Encoder %</Th>)}
|
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td>{gpu_usages[gpu]['gpu']}</Td>
|
<Td>{gpu_usages[gpu]['gpu']}</Td>
|
||||||
<Td>{gpu_usages[gpu]['mem']}</Td>
|
<Td>{gpu_usages[gpu]['mem']}</Td>
|
||||||
{'dec' in gpu_usages[gpu] && (<Td>{gpu_usages[gpu]['dec']}</Td>)}
|
|
||||||
{'enc' in gpu_usages[gpu] && (<Td>{gpu_usages[gpu]['enc']}</Td>)}
|
|
||||||
</Tr>
|
</Tr>
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -338,86 +334,80 @@ export default function System() {
|
|||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
<div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
<div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4">
|
||||||
{cameraNames.map(
|
{cameraNames.map((camera) => ( config.cameras[camera]["enabled"] && (
|
||||||
(camera) =>
|
<div key={camera} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow">
|
||||||
config.cameras[camera]['enabled'] && (
|
<div className="capitalize text-lg flex justify-between p-4">
|
||||||
<div
|
<Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
|
||||||
key={camera}
|
<Button onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
|
||||||
className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow"
|
</div>
|
||||||
>
|
<div className="p-2">
|
||||||
<div className="capitalize text-lg flex justify-between p-4">
|
<Table className="w-full">
|
||||||
<Link href={`/cameras/${camera}`}>{camera.replaceAll('_', ' ')}</Link>
|
<Thead>
|
||||||
<Button onClick={(e) => onHandleFfprobe(camera, e)}>ffprobe</Button>
|
<Tr>
|
||||||
</div>
|
<Th>Process</Th>
|
||||||
<div className="p-2">
|
<Th>P-ID</Th>
|
||||||
<Table className="w-full">
|
<Th>FPS</Th>
|
||||||
<Thead>
|
<Th>CPU %</Th>
|
||||||
<Tr>
|
<Th>Memory %</Th>
|
||||||
<Th>Process</Th>
|
{config.telemetry.network_bandwidth && <Th>Network Bandwidth</Th>}
|
||||||
<Th>P-ID</Th>
|
</Tr>
|
||||||
<Th>FPS</Th>
|
</Thead>
|
||||||
<Th>CPU %</Th>
|
<Tbody>
|
||||||
<Th>Memory %</Th>
|
<Tr key="ffmpeg" index="0">
|
||||||
{config.telemetry.network_bandwidth && <Th>Network Bandwidth</Th>}
|
<Td>
|
||||||
</Tr>
|
ffmpeg
|
||||||
</Thead>
|
<Button
|
||||||
<Tbody>
|
className="rounded-full"
|
||||||
<Tr key="ffmpeg" index="0">
|
type="text"
|
||||||
<Td>
|
color="gray"
|
||||||
ffmpeg
|
aria-label={cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cmdline']}
|
||||||
<Button
|
onClick={() => copy(cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cmdline'])}
|
||||||
className="rounded-full"
|
>
|
||||||
type="text"
|
<About className="w-3" />
|
||||||
color="gray"
|
</Button>
|
||||||
aria-label={cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cmdline']}
|
</Td>
|
||||||
onClick={() => copy(cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cmdline'])}
|
<Td>{cameras[camera]['ffmpeg_pid'] || '- '}</Td>
|
||||||
>
|
<Td>{cameras[camera]['camera_fps'] || '- '}</Td>
|
||||||
<About className="w-3" />
|
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cpu'] || '- '}%</Td>
|
||||||
</Button>
|
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['mem'] || '- '}%</Td>
|
||||||
</Td>
|
{config.telemetry.network_bandwidth && (
|
||||||
<Td>{cameras[camera]['ffmpeg_pid'] || '- '}</Td>
|
<Td>{bandwidth_usages[cameras[camera]['ffmpeg_pid']]?.['bandwidth'] || '- '}KB/s</Td>
|
||||||
<Td>{cameras[camera]['camera_fps'] || '- '}</Td>
|
)}
|
||||||
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['cpu'] || '- '}%</Td>
|
</Tr>
|
||||||
<Td>{cpu_usages[cameras[camera]['ffmpeg_pid']]?.['mem'] || '- '}%</Td>
|
<Tr key="capture" index="1">
|
||||||
{config.telemetry.network_bandwidth && (
|
<Td>Capture</Td>
|
||||||
<Td>{bandwidth_usages[cameras[camera]['ffmpeg_pid']]?.['bandwidth'] || '- '}KB/s</Td>
|
<Td>{cameras[camera]['capture_pid'] || '- '}</Td>
|
||||||
)}
|
<Td>{cameras[camera]['process_fps'] || '- '}</Td>
|
||||||
</Tr>
|
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['cpu'] || '- '}%</Td>
|
||||||
<Tr key="capture" index="1">
|
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['mem'] || '- '}%</Td>
|
||||||
<Td>Capture</Td>
|
{config.telemetry.network_bandwidth && <Td>-</Td>}
|
||||||
<Td>{cameras[camera]['capture_pid'] || '- '}</Td>
|
</Tr>
|
||||||
<Td>{cameras[camera]['process_fps'] || '- '}</Td>
|
<Tr key="detect" index="2">
|
||||||
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['cpu'] || '- '}%</Td>
|
<Td>Detect</Td>
|
||||||
<Td>{cpu_usages[cameras[camera]['capture_pid']]?.['mem'] || '- '}%</Td>
|
<Td>{cameras[camera]['pid'] || '- '}</Td>
|
||||||
{config.telemetry.network_bandwidth && <Td>-</Td>}
|
|
||||||
</Tr>
|
|
||||||
<Tr key="detect" index="2">
|
|
||||||
<Td>Detect</Td>
|
|
||||||
<Td>{cameras[camera]['pid'] || '- '}</Td>
|
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
if (cameras[camera]['pid'] && cameras[camera]['detection_enabled'] == 1)
|
if (cameras[camera]['pid'] && cameras[camera]['detection_enabled'] == 1)
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
{cameras[camera]['detection_fps']} ({cameras[camera]['skipped_fps']} skipped)
|
{cameras[camera]['detection_fps']} ({cameras[camera]['skipped_fps']} skipped)
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
else if (cameras[camera]['pid'] && cameras[camera]['detection_enabled'] == 0)
|
else if (cameras[camera]['pid'] && cameras[camera]['detection_enabled'] == 0)
|
||||||
return <Td>disabled</Td>;
|
return <Td>disabled</Td>;
|
||||||
|
|
||||||
return <Td>- </Td>;
|
return <Td>- </Td>;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<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>
|
||||||
{config.telemetry.network_bandwidth && <Td>-</Td>}
|
{config.telemetry.network_bandwidth && <Td>-</Td>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> )
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import * as CameraImage from '../../components/CameraImage';
|
import * as CameraImage from '../../components/CameraImage';
|
||||||
import * as Hooks from '../../hooks';
|
|
||||||
import * as WS from '../../api/ws';
|
import * as WS from '../../api/ws';
|
||||||
import Cameras from '../Cameras';
|
import Cameras from '../Cameras';
|
||||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from 'testing-library';
|
import { fireEvent, render, screen, waitForElementToBeRemoved } from 'testing-library';
|
||||||
@@ -9,7 +8,6 @@ describe('Cameras Route', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(CameraImage, 'default').mockImplementation(() => <div data-testid="camera-image" />);
|
vi.spyOn(CameraImage, 'default').mockImplementation(() => <div data-testid="camera-image" />);
|
||||||
vi.spyOn(WS, 'useWs').mockImplementation(() => ({ value: { payload: 'OFF' }, send: vi.fn() }));
|
vi.spyOn(WS, 'useWs').mockImplementation(() => ({ value: { payload: 'OFF' }, send: vi.fn() }));
|
||||||
vi.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 1000 }]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows an ActivityIndicator if not yet loaded', async () => {
|
test('shows an ActivityIndicator if not yet loaded', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import * as CameraImage from '../../components/CameraImage';
|
import * as CameraImage from '../../components/CameraImage';
|
||||||
import * as WS from '../../api/ws';
|
import * as WS from '../../api/ws';
|
||||||
import * as Hooks from '../../hooks';
|
|
||||||
import Cameras from '../Cameras';
|
import Cameras from '../Cameras';
|
||||||
import { render, screen, waitForElementToBeRemoved } from 'testing-library';
|
import { render, screen, waitForElementToBeRemoved } from 'testing-library';
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@ describe('Recording Route', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(CameraImage, 'default').mockImplementation(() => <div data-testid="camera-image" />);
|
vi.spyOn(CameraImage, 'default').mockImplementation(() => <div data-testid="camera-image" />);
|
||||||
vi.spyOn(WS, 'useWs').mockImplementation(() => ({ value: { payload: 'OFF' }, send: jest.fn() }));
|
vi.spyOn(WS, 'useWs').mockImplementation(() => ({ value: { payload: 'OFF' }, send: jest.fn() }));
|
||||||
vi.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 1000 }]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows an ActivityIndicator if not yet loaded', async () => {
|
test('shows an ActivityIndicator if not yet loaded', async () => {
|
||||||
|
|||||||
@@ -9,13 +9,6 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
'import.meta.vitest': 'undefined',
|
'import.meta.vitest': 'undefined',
|
||||||
},
|
},
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:5000'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
preact(),
|
preact(),
|
||||||
monacoEditorPlugin.default({
|
monacoEditorPlugin.default({
|
||||||
|
|||||||
Reference in New Issue
Block a user