forked from Github/frigate
Compare commits
23 Commits
v0.13.0-be
...
v0.13.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0858859939 | ||
|
|
1c27ee2d2b | ||
|
|
08586f8f65 | ||
|
|
cfd04d164e | ||
|
|
fa2e4993d9 | ||
|
|
080d7a2d88 | ||
|
|
7d0216b8fb | ||
|
|
c743dfd657 | ||
|
|
4a6579527b | ||
|
|
fd9196ae3e | ||
|
|
a3eccce8f3 | ||
|
|
111933d3b4 | ||
|
|
76d4f16db3 | ||
|
|
0d6bb6714a | ||
|
|
5d30944d6e | ||
|
|
3797340efa | ||
|
|
8728139ae3 | ||
|
|
730851cda9 | ||
|
|
46412e99d9 | ||
|
|
e5664826b1 | ||
|
|
9a1c8b2cc4 | ||
|
|
6aedc39a9a | ||
|
|
b9e6afa659 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push amd64 standard build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/main/Dockerfile
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
tags: ${{ steps.setup.outputs.image-name }}-amd64
|
||||
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64
|
||||
- name: Build and push TensorRT (x86 GPU)
|
||||
uses: docker/bake-action@v3
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
push: true
|
||||
targets: tensorrt
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push arm64 standard build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/main/Dockerfile
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
${{ steps.setup.outputs.image-name }}-standard-arm64
|
||||
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
||||
- name: Build and push RPi build
|
||||
uses: docker/bake-action@v3
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
push: true
|
||||
targets: rpi
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
BASE_IMAGE: timongentzsch/l4t-ubuntu20-opencv:latest
|
||||
SLIM_BASE: timongentzsch/l4t-ubuntu20-opencv:latest
|
||||
TRT_BASE: timongentzsch/l4t-ubuntu20-opencv:latest
|
||||
uses: docker/bake-action@v3
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
push: true
|
||||
targets: tensorrt
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
BASE_IMAGE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime
|
||||
SLIM_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime
|
||||
TRT_BASE: nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime
|
||||
uses: docker/bake-action@v3
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
push: true
|
||||
targets: tensorrt
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
@@ -97,9 +97,9 @@ jobs:
|
||||
run: npm run build
|
||||
working-directory: ./web
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build
|
||||
run: make
|
||||
- name: Run mypy
|
||||
|
||||
@@ -33,7 +33,7 @@ RUN --mount=type=tmpfs,target=/tmp --mount=type=tmpfs,target=/var/cache/apt \
|
||||
FROM scratch AS go2rtc
|
||||
ARG TARGETARCH
|
||||
WORKDIR /rootfs/usr/local/go2rtc/bin
|
||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.6.2/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.7.1/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
|
||||
|
||||
####
|
||||
|
||||
@@ -100,12 +100,25 @@ for name in go2rtc_config.get("streams", {}):
|
||||
stream = go2rtc_config["streams"][name]
|
||||
|
||||
if isinstance(stream, str):
|
||||
go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format(
|
||||
**FRIGATE_ENV_VARS
|
||||
)
|
||||
try:
|
||||
go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format(
|
||||
**FRIGATE_ENV_VARS
|
||||
)
|
||||
except KeyError as e:
|
||||
print(
|
||||
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
||||
)
|
||||
sys.exit(e)
|
||||
|
||||
elif isinstance(stream, list):
|
||||
for i, stream in enumerate(stream):
|
||||
go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS)
|
||||
try:
|
||||
go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS)
|
||||
except KeyError as e:
|
||||
print(
|
||||
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
||||
)
|
||||
sys.exit(e)
|
||||
|
||||
# add birdseye restream stream if enabled
|
||||
if config.get("birdseye", {}).get("restream", False):
|
||||
|
||||
@@ -43,6 +43,15 @@ if [[ -z ${MODEL_CONVERT} ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Setup ENV to select GPU for conversion
|
||||
if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then
|
||||
if [ ! -z ${CUDA_VISIBLE_DEVICES+x} ]; then
|
||||
PREVIOUS_CVD="$CUDA_VISIBLE_DEVICES"
|
||||
unset CUDA_VISIBLE_DEVICES
|
||||
fi
|
||||
export CUDA_VISIBLE_DEVICES="$TRT_MODEL_PREP_DEVICE"
|
||||
fi
|
||||
|
||||
# On Jetpack 4.6, the nvidia container runtime will mount several host nvidia libraries into the
|
||||
# container which should not be present in the image - if they are, TRT model generation will
|
||||
# fail or produce invalid models. Thus we must request the user to install them on the host in
|
||||
@@ -87,5 +96,14 @@ do
|
||||
echo "Generated ${model}.trt in $(($(date +%s)-start)) seconds"
|
||||
done
|
||||
|
||||
# Restore ENV after conversion
|
||||
if [ ! -z ${TRT_MODEL_PREP_DEVICE+x} ]; then
|
||||
unset CUDA_VISIBLE_DEVICES
|
||||
if [ ! -z ${PREVIOUS_CVD+x} ]; then
|
||||
export CUDA_VISIBLE_DEVICES="$PREVIOUS_CVD"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Print which models exist in output folder
|
||||
echo "Available tensorrt models:"
|
||||
cd ${OUTPUT_FOLDER} && ls *.trt;
|
||||
|
||||
@@ -120,7 +120,7 @@ NOTE: The folder that is mapped from the host needs to be the folder that contai
|
||||
|
||||
## Custom go2rtc version
|
||||
|
||||
Frigate currently includes go2rtc v1.6.2, there may be certain cases where you want to run a different version of go2rtc.
|
||||
Frigate currently includes go2rtc v1.7.1, there may be certain cases where you want to run a different version of go2rtc.
|
||||
|
||||
To do this:
|
||||
|
||||
|
||||
@@ -48,15 +48,26 @@ cameras:
|
||||
- detect
|
||||
```
|
||||
|
||||
### 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 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
|
||||
|
||||
Volume is considered motion for recordings, this means when the `record -> retain -> mode` is set to `motion` any time audio volume is > min_volume that recording segment for that camera will be kept.
|
||||
|
||||
:::
|
||||
|
||||
### Configuring Audio Events
|
||||
|
||||
The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `speech`, `yell`, and `scream` are enabled but these can be customized.
|
||||
The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `scream`, `speech`, and `yell` are enabled but these can be customized.
|
||||
|
||||
```yaml
|
||||
audio:
|
||||
enabled: True
|
||||
listen:
|
||||
- bark
|
||||
- fire_alarm
|
||||
- scream
|
||||
- speech
|
||||
- yell
|
||||
|
||||
@@ -80,8 +80,8 @@ cameras:
|
||||
rtmp:
|
||||
enabled: False # <-- RTMP should be disabled if your stream is not H264
|
||||
detect:
|
||||
width: # <- optional, by default Frigate tries to automatically detect resolution
|
||||
height: # <- optional, by default Frigate tries to automatically detect resolution
|
||||
width: # <- optional, by default Frigate tries to automatically detect resolution
|
||||
height: # <- optional, by default Frigate tries to automatically detect resolution
|
||||
```
|
||||
|
||||
### Blue Iris RTSP Cameras
|
||||
@@ -108,20 +108,20 @@ According to [this discussion](https://github.com/blakeblackshear/frigate/issues
|
||||
```yaml
|
||||
go2rtc:
|
||||
streams:
|
||||
your_reolink_camera:
|
||||
your_reolink_camera:
|
||||
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus"
|
||||
your_reolink_camera_sub:
|
||||
your_reolink_camera_sub:
|
||||
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
|
||||
|
||||
cameras:
|
||||
reolink:
|
||||
your_reolink_camera:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/your_reolink_camera?video=copy&audio=aac
|
||||
- path: rtsp://127.0.0.1:8554/your_reolink_camera
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- path: rtsp://127.0.0.1:8554/your_reolink_camera_sub?video=copy
|
||||
- path: rtsp://127.0.0.1:8554/your_reolink_camera_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- detect
|
||||
@@ -140,7 +140,7 @@ go2rtc:
|
||||
- rtspx://192.168.1.1:7441/abcdefghijk
|
||||
```
|
||||
|
||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#source-rtsp)
|
||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-rtsp)
|
||||
|
||||
In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record and rtmp if used directly with unifi protect.
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ audio:
|
||||
# Optional: Types of audio to listen for (default: shown below)
|
||||
listen:
|
||||
- bark
|
||||
- fire_alarm
|
||||
- scream
|
||||
- speech
|
||||
- yell
|
||||
@@ -361,6 +362,16 @@ record:
|
||||
# active_objects - save all recording segments with active/moving objects
|
||||
# NOTE: this mode only applies when the days setting above is greater than 0
|
||||
mode: all
|
||||
# Optional: Recording Export Settings
|
||||
export:
|
||||
# Optional: Timelapse Output Args (default: shown below).
|
||||
# NOTE: The default args are set to fit 24 hours of recording into 1 hour playback.
|
||||
# See https://stackoverflow.com/a/58268695 for more info on how these args work.
|
||||
# As an example: if you wanted to go from 24 hours to 30 minutes that would be going
|
||||
# from 86400 seconds to 1800 seconds which would be 1800 / 86400 = 0.02.
|
||||
# The -r (framerate) dictates how smooth the output video is.
|
||||
# So the args would be -vf setpts=0.02*PTS -r 30 in that case.
|
||||
timelapse_args: "-vf setpts=0.04*PTS -r 30"
|
||||
# Optional: Event recording settings
|
||||
events:
|
||||
# Optional: Number of seconds before the event to include (default: shown below)
|
||||
@@ -425,7 +436,7 @@ rtmp:
|
||||
enabled: False
|
||||
|
||||
# Optional: Restream configuration
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.6.2)
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.7.1)
|
||||
go2rtc:
|
||||
|
||||
# Optional: jsmpeg stream configuration for WebUI
|
||||
@@ -624,7 +635,7 @@ ui:
|
||||
|
||||
# Optional: Telemetry configuration
|
||||
telemetry:
|
||||
# Optional: Enabled network interfaces for bandwidth stats monitoring (default: shown below)
|
||||
# Optional: Enabled network interfaces for bandwidth stats monitoring (default: empty list, let nethogs search all)
|
||||
network_interfaces:
|
||||
- eth
|
||||
- enp
|
||||
@@ -639,6 +650,7 @@ telemetry:
|
||||
# Enable Intel GPU stats (default: shown below)
|
||||
intel_gpu_stats: True
|
||||
# Enable network bandwidth stats monitoring for camera ffmpeg processes, go2rtc, and object detectors. (default: shown below)
|
||||
# NOTE: The container must either be privileged or have cap_net_admin, cap_net_raw capabilities enabled.
|
||||
network_bandwidth: False
|
||||
# Optional: Enable the latest version outbound check (default: shown below)
|
||||
# NOTE: If you use the HomeAssistant integration, disabling this will prevent it from reporting new versions
|
||||
|
||||
@@ -78,7 +78,7 @@ WebRTC works by creating a TCP or UDP connection on port `8555`. However, it req
|
||||
- 192.168.1.10:8555
|
||||
- stun:8555
|
||||
```
|
||||
|
||||
|
||||
- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.0.0.0/8` CIDR block.
|
||||
|
||||
:::tip
|
||||
@@ -115,4 +115,4 @@ services:
|
||||
|
||||
:::
|
||||
|
||||
See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#module-webrtc) for more information about this.
|
||||
See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#module-webrtc) for more information about this.
|
||||
|
||||
@@ -235,10 +235,18 @@ An example `docker-compose.yml` fragment that converts the `yolov4-608` and `yol
|
||||
```yml
|
||||
frigate:
|
||||
environment:
|
||||
- YOLO_MODELS="yolov4-608,yolov7x-640"
|
||||
- YOLO_MODELS=yolov4-608,yolov7x-640
|
||||
- USE_FP16=false
|
||||
```
|
||||
|
||||
If you have multiple GPUs passed through to Frigate, you can specify which one to use for the model conversion. The conversion script will use the first visible GPU, however in systems with mixed GPU models you may not want to use the default index for object detection. Add the `TRT_MODEL_PREP_DEVICE` environment variable to select a specific GPU.
|
||||
|
||||
```yml
|
||||
frigate:
|
||||
environment:
|
||||
- TRT_MODEL_PREP_DEVICE=0 # Optionally, select which GPU is used for model optimization
|
||||
```
|
||||
|
||||
### Configuration Parameters
|
||||
|
||||
The TensorRT detector can be selected by specifying `tensorrt` as the model type. The GPU will need to be passed through to the docker container using the same methods described in the [Hardware Acceleration](hardware_acceleration.md#nvidia-gpu) section. If you pass through multiple GPUs, you can select which GPU is used for a detector with the `device` configuration parameter. The `device` parameter is an integer value of the GPU index, as shown by `nvidia-smi` within the container.
|
||||
|
||||
@@ -7,7 +7,7 @@ title: Restream
|
||||
|
||||
Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||
|
||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.6.2) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#configuration) for more advanced configurations and features.
|
||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.7.1) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#configuration) for more advanced configurations and features.
|
||||
|
||||
:::note
|
||||
|
||||
@@ -53,31 +53,31 @@ One connection is made to the camera. One for the restream, `detect` and `record
|
||||
```yaml
|
||||
go2rtc:
|
||||
streams:
|
||||
rtsp_cam: # <- for RTSP streams
|
||||
name_your_rtsp_cam: # <- for RTSP streams
|
||||
- rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio
|
||||
- "ffmpeg:rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus)
|
||||
http_cam: # <- for other streams
|
||||
- "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus)
|
||||
name_your_http_cam: # <- for other streams
|
||||
- http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio
|
||||
- "ffmpeg:http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus)
|
||||
- "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to the missing codec (usually will be opus)
|
||||
|
||||
cameras:
|
||||
rtsp_cam:
|
||||
name_your_rtsp_cam:
|
||||
ffmpeg:
|
||||
output_args:
|
||||
record: preset-record-generic-audio-copy
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/rtsp_cam # <--- the name here must match the name of the camera in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
- audio # <- only necessary if audio detection is enabled
|
||||
http_cam:
|
||||
name_your_http_cam:
|
||||
ffmpeg:
|
||||
output_args:
|
||||
record: preset-record-generic-audio-copy
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/http_cam # <--- the name here must match the name of the camera in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
@@ -92,44 +92,44 @@ Two connections are made to the camera. One for the sub stream, one for the rest
|
||||
```yaml
|
||||
go2rtc:
|
||||
streams:
|
||||
rtsp_cam:
|
||||
name_your_rtsp_cam:
|
||||
- rtsp://192.168.1.5:554/live0 # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg
|
||||
- "ffmpeg:rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
rtsp_cam_sub:
|
||||
- "ffmpeg:name_your_rtsp_cam#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
name_your_rtsp_cam_sub:
|
||||
- rtsp://192.168.1.5:554/substream # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg
|
||||
- "ffmpeg:rtsp_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
http_cam:
|
||||
- "ffmpeg:name_your_rtsp_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
name_your_http_cam:
|
||||
- http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg
|
||||
- "ffmpeg:http_cam#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
http_cam_sub:
|
||||
- "ffmpeg:name_your_http_cam#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
name_your_http_cam_sub:
|
||||
- http://192.168.50.155/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=user&password=password # <- stream which supports video & aac audio. This is only supported for rtsp streams, http must use ffmpeg
|
||||
- "ffmpeg:http_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
- "ffmpeg:name_your_http_cam_sub#audio=opus" # <- copy of the stream which transcodes audio to opus
|
||||
|
||||
cameras:
|
||||
rtsp_cam:
|
||||
name_your_rtsp_cam:
|
||||
ffmpeg:
|
||||
output_args:
|
||||
record: preset-record-generic-audio-copy
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/rtsp_cam # <--- the name here must match the name of the camera in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_rtsp_cam # <--- the name here must match the name of the camera in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- path: rtsp://127.0.0.1:8554/rtsp_cam_sub # <--- the name here must match the name of the camera_sub in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_rtsp_cam_sub # <--- the name here must match the name of the camera_sub in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- audio # <- only necessary if audio detection is enabled
|
||||
- detect
|
||||
http_cam:
|
||||
name_your_http_cam:
|
||||
ffmpeg:
|
||||
output_args:
|
||||
record: preset-record-generic-audio-copy
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/http_cam # <--- the name here must match the name of the camera in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_http_cam # <--- the name here must match the name of the camera in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- path: rtsp://127.0.0.1:8554/http_cam_sub # <--- the name here must match the name of the camera_sub in restream
|
||||
- path: rtsp://127.0.0.1:8554/name_your_http_cam_sub # <--- the name here must match the name of the camera_sub in restream
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- audio # <- only necessary if audio detection is enabled
|
||||
@@ -138,7 +138,7 @@ cameras:
|
||||
|
||||
## Advanced Restream Configurations
|
||||
|
||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
|
||||
|
||||
NOTE: The output will need to be passed with two curly braces `{{output}}`
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect
|
||||
|
||||
# Setup a go2rtc stream
|
||||
|
||||
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. If you set the stream name under go2rtc to match the name of your camera, it will automatically be mapped and you will get additional live view options for the camera. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#module-streams), not just rtsp.
|
||||
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. If you set the stream name under go2rtc to match the name of your camera, it will automatically be mapped and you will get additional live view options for the camera. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#module-streams), not just rtsp.
|
||||
|
||||
```yaml
|
||||
go2rtc:
|
||||
@@ -24,7 +24,7 @@ The easiest live view to get working is MSE. After adding this to the config, re
|
||||
|
||||
### What if my video doesn't play?
|
||||
|
||||
If you are unable to see your video feed, first check the go2rtc logs in the Frigate UI under Logs in the sidebar. If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. If you do not see any errors, then the video codec of the stream may not be supported in your browser. If your camera stream is set to H265, try switching to H264. You can see more information about [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#codecs-madness) in the go2rtc documentation. If you are not able to switch your camera settings from H265 to H264 or your stream is a different format such as MJPEG, you can use go2rtc to re-encode the video using the [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.6.2#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. Here is an example of a config that will re-encode the stream to H264 without hardware acceleration:
|
||||
If you are unable to see your video feed, first check the go2rtc logs in the Frigate UI under Logs in the sidebar. If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log. If you do not see any errors, then the video codec of the stream may not be supported in your browser. If your camera stream is set to H265, try switching to H264. You can see more information about [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#codecs-madness) in the go2rtc documentation. If you are not able to switch your camera settings from H265 to H264 or your stream is a different format such as MJPEG, you can use go2rtc to re-encode the video using the [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.7.1#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view. Here is an example of a config that will re-encode the stream to H264 without hardware acceleration:
|
||||
|
||||
```yaml
|
||||
go2rtc:
|
||||
|
||||
@@ -313,7 +313,7 @@ Get PTZ info for the camera.
|
||||
|
||||
### `POST /api/events/<camera_name>/<label>/create`
|
||||
|
||||
Create a manual API 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.
|
||||
|
||||
**Optional Body:**
|
||||
|
||||
|
||||
@@ -3,13 +3,7 @@ id: plus
|
||||
title: Frigate+
|
||||
---
|
||||
|
||||
:::info
|
||||
|
||||
Frigate+ is under active development. Models are available as a part of an invitation only beta. It is free to create an account and upload/annotate your examples.
|
||||
|
||||
:::
|
||||
|
||||
Frigate+ offers models trained from scratch and specifically designed for the way Frigate NVR analyzes video footage. They offer higher accuracy with less resources and include a more relevant set of objects for security cameras. By uploading your own labeled examples, your model can be uniquely tuned for accuracy in your specific conditions. After tuning, performance is evaluated against a broad dataset and real world examples submitted by other Frigate+ users to prevent overfitting.
|
||||
For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/).
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
116
docs/docs/plus/index.md
Normal file
116
docs/docs/plus/index.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
id: index
|
||||
title: Models Guide
|
||||
---
|
||||
|
||||
Frigate+ offers models trained from scratch and specifically designed for the way Frigate NVR analyzes video footage. These models offer higher accuracy with less resources. By uploading your own labeled examples, your model is tuned for accuracy in your specific conditions. After tuning, performance is evaluated against a broad dataset and real world examples submitted by other Frigate+ users to prevent overfitting.
|
||||
|
||||
With a subscription, and at each annual renewal, you will receive 12 model training credits that can be used to train tuned models. If you cancel your subscription, you will keep your existing credits and retain access to any trained models. Users with an active subscription can purchase additional training credits for $5 each.
|
||||
|
||||
Information on how to integrate Frigate+ with Frigate can be found in the [integrations docs](/integrations/plus).
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
While developing these models, there were some common questions that arose.
|
||||
|
||||
### Are my video feeds sent to the cloud for analysis when using Frigate+ models?
|
||||
|
||||
No. Frigate+ models are a drop in replacement for the default model. All processing is performed locally as always. The only images sent to Frigate+ are the ones you specifically submit via the `Send to Frigate+` button or upload directly.
|
||||
|
||||
### Can I label anything I want and train the model to recognize something custom for me?
|
||||
|
||||
Not currently. At the moment, the set of labels will be consistent for all users. The focus will be on expanding that set of labels before working on completely custom user labels.
|
||||
|
||||
### Can Frigate+ models be used offline?
|
||||
|
||||
Yes. Models and metadata are stored in the `model_cache` directory within the config folder. Frigate will only attempt to download a model if it does not exist in the cache. This means you can backup the directory and/or use it completely offline.
|
||||
|
||||
### Can I keep using my Frigate+ models even if I do not renew my subscription?
|
||||
|
||||
Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models trained using the training credits that you purchased are yours to keep and use forever. However, do note that the terms and conditions prohibit you from sharing, reselling, or creating derivative products from the models.
|
||||
|
||||
## Important model information
|
||||
|
||||
### Supported Model Types
|
||||
|
||||
Currently, Frigate+ models only support CPU (`cpu`) and Coral (`edgetpu`) models. OpenVino is next in line to gain support.
|
||||
|
||||
The models are created using the same MobileDet architecture as the default model. Additional architectures will be added in future releases as needed.
|
||||
|
||||
### Higher Scores
|
||||
|
||||
Frigate+ models generally have much higher scores than the default model provided in Frigate. You will likely need to increase your `threshold` and `min_score` values. Here is an example of how these values can be refined, but you should expect these to evolve as your model improves:
|
||||
|
||||
```yaml
|
||||
objects:
|
||||
filters:
|
||||
dog:
|
||||
min_score: .7
|
||||
threshold: .9
|
||||
cat:
|
||||
min_score: .65
|
||||
threshold: .8
|
||||
face:
|
||||
min_score: .7
|
||||
package:
|
||||
min_score: .65
|
||||
threshold: .9
|
||||
license_plate:
|
||||
min_score: .6
|
||||
amazon:
|
||||
min_score: .75
|
||||
ups:
|
||||
min_score: .75
|
||||
fedex:
|
||||
min_score: .75
|
||||
person:
|
||||
min_score: .8
|
||||
threshold: .85
|
||||
car:
|
||||
min_score: .8
|
||||
threshold: .85
|
||||
```
|
||||
|
||||
### Available label types
|
||||
|
||||
Frigate+ models support a more relevant set of objects for security cameras. Currently, only the following objects are supported: `person`, `face`, `car`, `license_plate`, `amazon`, `ups`, `fedex`, `package`, `dog`, `cat`, `deer`. Other object types available in the default Frigate model are not available. Additional object types will be added in future releases.
|
||||
|
||||
#### Label attributes
|
||||
|
||||
Frigate has special handling for some labels when using Frigate+ models. `face`, `license_plate`, `amazon`, `ups`, and `fedex` are considered attribute labels which are not tracked like regular objects and do not generate events. In addition, the `threshold` filter will have no effect on these labels. You should adjust the `min_score` and other filter values as needed.
|
||||
|
||||
When using Frigate+ models, Frigate will choose the snapshot of a person object that has the largest visible face. For cars, the snapshot with the largest visible license plate will be selected. This aids in secondary processing such as facial and license plate recognition for person and car objects.
|
||||
|
||||

|
||||
|
||||
`amazon`, `ups`, and `fedex` labels are used to automatically assign a sub label to car objects.
|
||||
|
||||

|
||||
|
||||
## Properly labeling images
|
||||
|
||||
For the best results, follow the following guidelines.
|
||||
|
||||
**Label every object in the image**: It is important that you label all objects in each image before verifying. If you don't label a car for example, the model will be taught that part of the image is _not_ a car and it will start to get confused.
|
||||
|
||||
**Make tight bounding boxes**: Tighter bounding boxes improve the recognition and ensure that accurate bounding boxes are predicted at runtime.
|
||||
|
||||
**Label the full object even when occluded**: If you have a person standing behind a car, label the full person even though a portion of their body may be hidden behind the car. This helps predict accurate bounding boxes and improves zone accuracy and filters at runtime.
|
||||
|
||||
**`amazon`, `ups`, and `fedex` should label the logo**: For a Fedex truck, label the truck as a `car` and make a different bounding box just for the Fedex logo. If there are multiple logos, label each of them.
|
||||
|
||||

|
||||
|
||||
## Improving your model
|
||||
|
||||
You may find that Frigate+ models result in more false positives initially, but by submitting true and false positives, the model will improve. This may be because your cameras don't look quite enough like the user submissions that were used to train the base model. Over time, this will improve as more and more users (including you) submit examples to Frigate+.
|
||||
|
||||
False positives can be reduced by submitting **both** true positives and false positives. This will help the model differentiate between what is and isn't correct.
|
||||
|
||||
You may find that it's helpful to lower your thresholds a little in order to generate more false/true positives near the threshold value. For example, if you have some false positives that are scoring at 68% and some true positives scoring at 72%, you can try lowering your threshold to 65% and submitting both true and false positives within that range. This will help the model learn and widen the gap between true and false positive scores.
|
||||
|
||||
Note that only verified images will be used when training your model. Submitting an image from Frigate as a true or false positive will not verify the image. You still must verify the image in Frigate+ in order for it to be used in training.
|
||||
|
||||
In order to request your first model, you will need to have annotated and verified at least 10 images. Each subsequent model request will require that 10 additional images are verified. However, this is the bare minimum. For the best results, you should provide at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night.
|
||||
|
||||
As circumstances change, you may need to submit new examples to address new types of false positives. For example, the change from summer days to snowy winter days or other changes such as a new grill or patio furniture may require additional examples and training.
|
||||
@@ -57,6 +57,9 @@ module.exports = {
|
||||
"integrations/mqtt",
|
||||
"integrations/third_party_extensions",
|
||||
],
|
||||
"Frigate+": [
|
||||
"plus/index"
|
||||
],
|
||||
Troubleshooting: [
|
||||
"troubleshooting/faqs",
|
||||
"troubleshooting/recordings",
|
||||
|
||||
BIN
docs/static/img/plus/attribute-example-face.jpg
vendored
Normal file
BIN
docs/static/img/plus/attribute-example-face.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/static/img/plus/attribute-example-fedex.jpg
vendored
Normal file
BIN
docs/static/img/plus/attribute-example-fedex.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/static/img/plus/fedex-logo.jpg
vendored
Normal file
BIN
docs/static/img/plus/fedex-logo.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -48,9 +48,10 @@ DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
|
||||
DEFAULT_TRACKED_OBJECTS = ["person"]
|
||||
DEFAULT_LISTEN_AUDIO = ["bark", "speech", "yell", "scream"]
|
||||
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
|
||||
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
|
||||
DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720}
|
||||
DEFAULT_TIME_LAPSE_FFMPEG_ARGS = "-vf setpts=0.04*PTS -r 30"
|
||||
|
||||
|
||||
class FrigateBaseModel(BaseModel):
|
||||
@@ -107,7 +108,7 @@ class StatsConfig(FrigateBaseModel):
|
||||
|
||||
class TelemetryConfig(FrigateBaseModel):
|
||||
network_interfaces: List[str] = Field(
|
||||
default=["eth", "enp", "eno", "ens", "wl", "lo"],
|
||||
default=[],
|
||||
title="Enabled network interfaces for bandwidth calculation.",
|
||||
)
|
||||
stats: StatsConfig = Field(
|
||||
@@ -198,6 +199,12 @@ class RecordRetainConfig(FrigateBaseModel):
|
||||
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
|
||||
|
||||
|
||||
class RecordExportConfig(FrigateBaseModel):
|
||||
timelapse_args: str = Field(
|
||||
default=DEFAULT_TIME_LAPSE_FFMPEG_ARGS, title="Timelapse Args"
|
||||
)
|
||||
|
||||
|
||||
class RecordConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=False, title="Enable record on all cameras.")
|
||||
sync_on_startup: bool = Field(
|
||||
@@ -213,6 +220,9 @@ class RecordConfig(FrigateBaseModel):
|
||||
events: EventsConfig = Field(
|
||||
default_factory=EventsConfig, title="Event specific settings."
|
||||
)
|
||||
export: RecordExportConfig = Field(
|
||||
default_factory=RecordExportConfig, title="Recording Export Config"
|
||||
)
|
||||
enabled_in_config: Optional[bool] = Field(
|
||||
title="Keep track of original state of recording."
|
||||
)
|
||||
@@ -387,7 +397,6 @@ class ZoneConfig(BaseModel):
|
||||
default=3,
|
||||
title="Number of consecutive frames required for object to be considered present in the zone.",
|
||||
gt=0,
|
||||
le=10,
|
||||
)
|
||||
objects: List[str] = Field(
|
||||
default_factory=list,
|
||||
|
||||
@@ -97,8 +97,8 @@ PRESETS_HW_ACCEL_ENCODE_TIMELAPSE = {
|
||||
"preset-rpi-32-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
|
||||
"preset-rpi-64-h264": "ffmpeg -hide_banner {0} -c:v h264_v4l2m2m {1}",
|
||||
"preset-vaapi": "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {2} {0} -c:v h264_vaapi {1}",
|
||||
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
"preset-intel-qsv-h264": "ffmpeg -hide_banner {0} -c:v h264_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
"preset-intel-qsv-h265": "ffmpeg -hide_banner {0} -c:v hevc_qsv -profile:v high -level:v 4.1 -async_depth:v 1 {1}",
|
||||
"preset-nvidia-h264": "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v h264_nvenc {1}",
|
||||
"preset-nvidia-h265": "ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 {0} -c:v hevc_nvenc {1}",
|
||||
"preset-jetson-h264": "ffmpeg -hide_banner {0} -c:v h264_nvmpi -profile high {1}",
|
||||
|
||||
@@ -34,6 +34,7 @@ from frigate.const import (
|
||||
CACHE_DIR,
|
||||
CLIPS_DIR,
|
||||
CONFIG_DIR,
|
||||
EXPORT_DIR,
|
||||
MAX_SEGMENT_DURATION,
|
||||
RECORD_DIR,
|
||||
)
|
||||
@@ -1646,7 +1647,12 @@ def export_recording(camera_name: str, start_time, end_time):
|
||||
)
|
||||
|
||||
if recordings_count <= 0:
|
||||
return "No recordings found for time range", 400
|
||||
return make_response(
|
||||
jsonify(
|
||||
{"success": False, "message": "No recordings found for time range"}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
exporter = RecordingExporter(
|
||||
current_app.frigate_config,
|
||||
@@ -1661,6 +1667,20 @@ def export_recording(camera_name: str, start_time, end_time):
|
||||
return "Starting export of recording", 200
|
||||
|
||||
|
||||
@bp.route("/export/<file_name>", methods=["DELETE"])
|
||||
def export_delete(file_name: str):
|
||||
file = os.path.join(EXPORT_DIR, file_name)
|
||||
|
||||
if not os.path.exists(file):
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": f"{file_name} not found."}),
|
||||
404,
|
||||
)
|
||||
|
||||
os.unlink(file)
|
||||
return "Successfully deleted file", 200
|
||||
|
||||
|
||||
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
|
||||
while True:
|
||||
# max out at specified FPS
|
||||
|
||||
@@ -580,7 +580,7 @@ class BirdsEyeFrameManager:
|
||||
except Exception:
|
||||
updated_frame = False
|
||||
self.active_cameras = []
|
||||
self.camera_layout = 0
|
||||
self.camera_layout = []
|
||||
print(traceback.format_exc())
|
||||
|
||||
# if the frame was updated or the fps is too low, send frame
|
||||
|
||||
@@ -121,6 +121,9 @@ class PtzAutoTrackerThread(threading.Thread):
|
||||
def run(self):
|
||||
while not self.stop_event.wait(1):
|
||||
for camera_name, cam in self.config.cameras.items():
|
||||
if not cam.enabled:
|
||||
continue
|
||||
|
||||
if cam.onvif.autotracking.enabled:
|
||||
self.ptz_autotracker.camera_maintenance(camera_name)
|
||||
else:
|
||||
@@ -153,6 +156,9 @@ class PtzAutoTracker:
|
||||
|
||||
# if cam is set to autotrack, onvif should be set up
|
||||
for camera_name, cam in self.config.cameras.items():
|
||||
if not cam.enabled:
|
||||
continue
|
||||
|
||||
self.autotracker_init[camera_name] = False
|
||||
if cam.onvif.autotracking.enabled:
|
||||
self._autotracker_setup(cam, camera_name)
|
||||
|
||||
@@ -13,10 +13,18 @@ from frigate.ffmpeg_presets import (
|
||||
EncodeTypeEnum,
|
||||
parse_preset_hardware_acceleration_encode,
|
||||
)
|
||||
from frigate.models import Recordings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey"
|
||||
|
||||
|
||||
def lower_priority():
|
||||
os.nice(10)
|
||||
|
||||
|
||||
class PlaybackFactorEnum(str, Enum):
|
||||
realtime = "realtime"
|
||||
timelapse_25x = "timelapse_25x"
|
||||
@@ -58,13 +66,31 @@ class RecordingExporter(threading.Thread):
|
||||
)
|
||||
else:
|
||||
playlist_lines = []
|
||||
playlist_start = self.start_time
|
||||
|
||||
while playlist_start < self.end_time:
|
||||
playlist_lines.append(
|
||||
f"file 'http://127.0.0.1:5000/vod/{self.camera}/start/{playlist_start}/end/{min(playlist_start + MAX_PLAYLIST_SECONDS, self.end_time)}/index.m3u8'"
|
||||
# get full set of recordings
|
||||
export_recordings = (
|
||||
Recordings.select()
|
||||
.where(
|
||||
Recordings.start_time.between(self.start_time, self.end_time)
|
||||
| Recordings.end_time.between(self.start_time, self.end_time)
|
||||
| (
|
||||
(self.start_time > Recordings.start_time)
|
||||
& (self.end_time < Recordings.end_time)
|
||||
)
|
||||
)
|
||||
.where(Recordings.camera == self.camera)
|
||||
.order_by(Recordings.start_time.asc())
|
||||
)
|
||||
|
||||
# Use pagination to process records in chunks
|
||||
page_size = 1000
|
||||
num_pages = (export_recordings.count() + page_size - 1) // page_size
|
||||
|
||||
for page in range(1, num_pages + 1):
|
||||
playlist = export_recordings.paginate(page, page_size)
|
||||
playlist_lines.append(
|
||||
f"file 'http://127.0.0.1:5000/vod/{self.camera}/start/{float(playlist[0].start_time)}/end/{float(playlist[-1].end_time)}/index.m3u8'"
|
||||
)
|
||||
playlist_start += MAX_PLAYLIST_SECONDS
|
||||
|
||||
ffmpeg_input = "-y -protocol_whitelist pipe,file,http,tcp -f concat -safe 0 -i /dev/stdin"
|
||||
|
||||
@@ -76,8 +102,8 @@ class RecordingExporter(threading.Thread):
|
||||
ffmpeg_cmd = (
|
||||
parse_preset_hardware_acceleration_encode(
|
||||
self.config.ffmpeg.hwaccel_args,
|
||||
ffmpeg_input,
|
||||
f"-vf setpts=0.04*PTS -r 30 -an {file_name}",
|
||||
f"{TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}",
|
||||
f"{self.config.cameras[self.camera].record.export.timelapse_args} {file_name}",
|
||||
EncodeTypeEnum.timelapse,
|
||||
)
|
||||
).split(" ")
|
||||
@@ -86,6 +112,7 @@ class RecordingExporter(threading.Thread):
|
||||
ffmpeg_cmd,
|
||||
input="\n".join(playlist_lines),
|
||||
encoding="ascii",
|
||||
preexec_fn=lower_priority,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@ class NorfairTracker(ObjectTracker):
|
||||
"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):
|
||||
del self.tracked_objects[id]
|
||||
del self.disappeared[id]
|
||||
|
||||
@@ -143,6 +143,9 @@ def get_cpu_stats() -> dict[str, dict]:
|
||||
|
||||
|
||||
def get_physical_interfaces(interfaces) -> list:
|
||||
if not interfaces:
|
||||
return []
|
||||
|
||||
with open("/proc/net/dev", "r") as file:
|
||||
lines = file.readlines()
|
||||
|
||||
@@ -171,6 +174,7 @@ def get_bandwidth_stats(config) -> dict[str, dict]:
|
||||
)
|
||||
|
||||
if p.returncode != 0:
|
||||
logger.error(f"Error getting network stats :: {p.stderr}")
|
||||
return usages
|
||||
else:
|
||||
lines = p.stdout.split("\n")
|
||||
|
||||
@@ -98,13 +98,8 @@ def filtered(obj, objects_to_track, object_filters):
|
||||
|
||||
|
||||
def get_min_region_size(model_config: ModelConfig) -> int:
|
||||
"""Get the min region size and ensure it is divisible by 4."""
|
||||
half = int(max(model_config.height, model_config.width) / 2)
|
||||
|
||||
if half % 4 == 0:
|
||||
return half
|
||||
|
||||
return int((half + 3) / 4) * 4
|
||||
"""Get the min region size."""
|
||||
return max(model_config.height, model_config.width)
|
||||
|
||||
|
||||
def create_tensor_input(frame, model_config: ModelConfig, region):
|
||||
|
||||
846
web/package-lock.json
generated
846
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useEffect, useCallback, useState } from 'preact/hooks';
|
||||
import useSWR from 'swr';
|
||||
import { usePtzCommand } from '../api/ws';
|
||||
import ActivityIndicator from './ActivityIndicator';
|
||||
@@ -27,29 +27,25 @@ export default function CameraControlPanel({ camera = '' }) {
|
||||
setCurrentPreset('');
|
||||
};
|
||||
|
||||
const onSetMove = async (e, dir) => {
|
||||
const onSetMove = useCallback(async (e, dir) => {
|
||||
e.stopPropagation();
|
||||
sendPtz(`MOVE_${dir}`);
|
||||
setCurrentPreset('');
|
||||
};
|
||||
}, [sendPtz, setCurrentPreset]);
|
||||
|
||||
const onSetZoom = async (e, dir) => {
|
||||
const onSetZoom = useCallback(async (e, dir) => {
|
||||
e.stopPropagation();
|
||||
sendPtz(`ZOOM_${dir}`);
|
||||
setCurrentPreset('');
|
||||
};
|
||||
}, [sendPtz, setCurrentPreset]);
|
||||
|
||||
const onSetStop = async (e) => {
|
||||
const onSetStop = useCallback(async (e) => {
|
||||
e.stopPropagation();
|
||||
sendPtz('STOP');
|
||||
};
|
||||
}, [sendPtz]);
|
||||
|
||||
if (!ptz) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!e) {
|
||||
const keydownListener = useCallback((e) => {
|
||||
if (!ptz || !e) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,9 +79,9 @@ export default function CameraControlPanel({ camera = '' }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [onSetMove, onSetZoom, ptz]);
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
const keyupListener = useCallback((e) => {
|
||||
if (!e || e.repeat) {
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +97,20 @@ export default function CameraControlPanel({ camera = '' }) {
|
||||
e.preventDefault();
|
||||
onSetStop(e);
|
||||
}
|
||||
});
|
||||
}, [onSetStop]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', keydownListener);
|
||||
document.addEventListener('keyup', keyupListener);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keydownListener);
|
||||
document.removeEventListener('keyup', keyupListener);
|
||||
};
|
||||
}, [keydownListener, keyupListener, ptz]);
|
||||
|
||||
if (!ptz) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="control-panel" className="p-4 text-center sm:flex justify-start">
|
||||
|
||||
35
web/src/components/DialogLarge.jsx
Normal file
35
web/src/components/DialogLarge.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { createPortal } from 'preact/compat';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
export default function LargeDialog({ children, portalRootID = 'dialogs' }) {
|
||||
const portalRoot = portalRootID && document.getElementById(portalRootID);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
setShow(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const dialog = (
|
||||
<Fragment>
|
||||
<div
|
||||
data-testid="scrim"
|
||||
key="scrim"
|
||||
className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
|
||||
>
|
||||
<div
|
||||
role="modal"
|
||||
className={`absolute rounded shadow-2xl bg-white dark:bg-gray-700 w-4/5 max-w-7xl text-gray-900 dark:text-white transition-transform transition-opacity duration-75 transform scale-90 opacity-0 ${
|
||||
show ? 'scale-100 opacity-100' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return portalRoot ? createPortal(dialog, portalRoot) : dialog;
|
||||
}
|
||||
@@ -217,6 +217,7 @@ class VideoRTC extends HTMLElement {
|
||||
this.video.controls = true;
|
||||
this.video.playsInline = true;
|
||||
this.video.preload = 'auto';
|
||||
this.video.muted = true;
|
||||
|
||||
this.video.style.display = 'block'; // fix bottom margin 4px
|
||||
this.video.style.width = '100%';
|
||||
|
||||
@@ -28,10 +28,13 @@ export default function Config() {
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setError('');
|
||||
setSuccess(response.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setSuccess('');
|
||||
|
||||
if (error.response) {
|
||||
setError(error.response.data.message);
|
||||
} else {
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function Events({ path, ...props }) {
|
||||
zones: props.zones ?? 'all',
|
||||
sub_labels: props.sub_labels ?? 'all',
|
||||
favorites: props.favorites ?? 0,
|
||||
event: props.event,
|
||||
});
|
||||
const [state, setState] = useState({
|
||||
showDownloadMenu: false,
|
||||
@@ -69,7 +70,7 @@ export default function Events({ path, ...props }) {
|
||||
validBox: null,
|
||||
});
|
||||
const [uploading, setUploading] = useState([]);
|
||||
const [viewEvent, setViewEvent] = useState();
|
||||
const [viewEvent, setViewEvent] = useState(props.event);
|
||||
const [eventOverlay, setEventOverlay] = useState();
|
||||
const [eventDetailType, setEventDetailType] = useState('clip');
|
||||
const [downloadEvent, setDownloadEvent] = useState({
|
||||
@@ -87,9 +88,13 @@ export default function Events({ path, ...props }) {
|
||||
});
|
||||
|
||||
const eventsFetcher = useCallback((path, params) => {
|
||||
if (searchParams.event) {
|
||||
path = `${path}/${searchParams.event}`;
|
||||
return axios.get(path).then((res) => [res.data]);
|
||||
}
|
||||
params = { ...params, include_thumbnails: 0, limit: API_LIMIT };
|
||||
return axios.get(path, { params }).then((res) => res.data);
|
||||
}, []);
|
||||
}, [searchParams]);
|
||||
|
||||
const getKey = useCallback(
|
||||
(index, prevData) => {
|
||||
@@ -355,6 +360,11 @@ export default function Events({ path, ...props }) {
|
||||
onSelectSingle={(item) => onFilter('sub_labels', item)}
|
||||
/>
|
||||
)}
|
||||
{searchParams.event && (
|
||||
<Button className="ml-2" onClick={() => onFilter('event',null)} type="text">
|
||||
View All
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<StarRecording
|
||||
className="h-10 w-10 text-yellow-300 cursor-pointer ml-auto"
|
||||
@@ -556,6 +566,9 @@ export default function Events({ path, ...props }) {
|
||||
<p className="mb-2">Confirm deletion of saved event.</p>
|
||||
</div>
|
||||
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||
<Button className="ml-2" onClick={() => setDeleteFavoriteState({ ...state, showDeleteFavorite: false })} type="text">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-2"
|
||||
color="red"
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import Heading from '../components/Heading';
|
||||
import { useState } from 'preact/hooks';
|
||||
import useSWR from 'swr';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import Button from '../components/Button';
|
||||
import axios from 'axios';
|
||||
import { baseUrl } from '../api/baseUrl';
|
||||
import { Fragment } from 'preact';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import { Play } from '../icons/Play';
|
||||
import { Delete } from '../icons/Delete';
|
||||
import LargeDialog from '../components/DialogLarge';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import Dialog from '../components/Dialog';
|
||||
|
||||
export default function Export() {
|
||||
const { data: config } = useSWR('config');
|
||||
const { data: exports } = useSWR('exports/', (url) => axios({ baseURL: baseUrl, url }).then((res) => res.data));
|
||||
|
||||
// Export States
|
||||
const [camera, setCamera] = useState('select');
|
||||
const [playback, setPlayback] = useState('select');
|
||||
const [message, setMessage] = useState({ text: '', error: false });
|
||||
@@ -26,6 +32,11 @@ export default function Export() {
|
||||
const [endDate, setEndDate] = useState(localISODate);
|
||||
const [endTime, setEndTime] = useState('23:59');
|
||||
|
||||
// Export States
|
||||
|
||||
const [selectedClip, setSelectedClip] = useState();
|
||||
const [deleteClip, setDeleteClip] = useState();
|
||||
|
||||
const onHandleExport = () => {
|
||||
if (camera == 'select') {
|
||||
setMessage({ text: 'A camera needs to be selected.', error: true });
|
||||
@@ -58,7 +69,7 @@ export default function Export() {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response) {
|
||||
if (error.response?.data?.message) {
|
||||
setMessage({ text: `Failed to start export: ${error.response.data.message}`, error: true });
|
||||
} else {
|
||||
setMessage({ text: `Failed to start export: ${error.message}`, error: true });
|
||||
@@ -66,6 +77,15 @@ export default function Export() {
|
||||
});
|
||||
};
|
||||
|
||||
const onHandleDelete = (clip) => {
|
||||
axios.delete(`export/${clip}`).then((response) => {
|
||||
if (response.status == 200) {
|
||||
setDeleteClip();
|
||||
mutate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2 px-4 w-full">
|
||||
<Heading>Export</Heading>
|
||||
@@ -74,6 +94,55 @@ export default function Export() {
|
||||
<div className={`max-h-20 ${message.error ? 'text-red-500' : 'text-green-500'}`}>{message.text}</div>
|
||||
)}
|
||||
|
||||
{selectedClip && (
|
||||
<LargeDialog>
|
||||
<div>
|
||||
<Heading className="p-2">Playback</Heading>
|
||||
<VideoPlayer
|
||||
options={{
|
||||
preload: 'auto',
|
||||
autoplay: true,
|
||||
sources: [
|
||||
{
|
||||
src: `${baseUrl}exports/${selectedClip}`,
|
||||
type: 'video/mp4',
|
||||
},
|
||||
],
|
||||
}}
|
||||
seekOptions={{ forward: 10, backward: 5 }}
|
||||
onReady={(player) => {
|
||||
this.player = player;
|
||||
}}
|
||||
onDispose={() => {
|
||||
this.player = null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||
<Button className="ml-2" onClick={() => setSelectedClip('')} type="text">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</LargeDialog>
|
||||
)}
|
||||
|
||||
{deleteClip && (
|
||||
<Dialog>
|
||||
<div className="p-4">
|
||||
<Heading size="lg">Delete Export?</Heading>
|
||||
<p className="py-4 mb-2">Confirm deletion of {deleteClip}.</p>
|
||||
</div>
|
||||
<div className="p-2 flex justify-start flex-row-reverse space-x-2">
|
||||
<Button className="ml-2" onClick={() => setDeleteClip('')} type="text">
|
||||
Close
|
||||
</Button>
|
||||
<Button className="ml-2" color="red" onClick={() => onHandleDelete(deleteClip)} type="text">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<div className="xl:flex justify-between">
|
||||
<div>
|
||||
<div>
|
||||
@@ -144,7 +213,11 @@ export default function Export() {
|
||||
{exports && (
|
||||
<div className="p-4 bg-gray-800 xl:w-1/2">
|
||||
<Heading size="md">Exports</Heading>
|
||||
<Exports exports={exports} />
|
||||
<Exports
|
||||
exports={exports}
|
||||
onSetClip={(clip) => setSelectedClip(clip)}
|
||||
onDeleteClip={(clip) => setDeleteClip(clip)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -152,7 +225,7 @@ export default function Export() {
|
||||
);
|
||||
}
|
||||
|
||||
function Exports({ exports }) {
|
||||
function Exports({ exports, onSetClip, onDeleteClip }) {
|
||||
return (
|
||||
<Fragment>
|
||||
{exports.map((item) => (
|
||||
@@ -166,9 +239,19 @@ function Exports({ exports }) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-start items-center">
|
||||
<a className="text-blue-500 hover:underline" href={`${baseUrl}exports/${item.name}`} download>
|
||||
<Button type="iconOnly" onClick={() => onSetClip(item.name)}>
|
||||
<Play className="h-6 w-6 text-green-600" />
|
||||
</Button>
|
||||
<a
|
||||
className="text-blue-500 hover:underline overflow-hidden"
|
||||
href={`${baseUrl}exports/${item.name}`}
|
||||
download
|
||||
>
|
||||
{item.name.substring(0, item.name.length - 4)}
|
||||
</a>
|
||||
<Button className="ml-auto" type="iconOnly" onClick={() => onDeleteClip(item.name)}>
|
||||
<Delete className="h-6 w-6" stroke="#f87171" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user