forked from Github/frigate
Compare commits
24 Commits
v0.13.0-be
...
v0.13.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa93d4fbdd | ||
|
|
d0036b2f77 | ||
|
|
1b57f8c7e2 | ||
|
|
fa96ec64e4 | ||
|
|
e89db13282 | ||
|
|
fe6577736e | ||
|
|
64537672e6 | ||
|
|
ef36aabd30 | ||
|
|
ca84732574 | ||
|
|
0b828ef1ec | ||
|
|
3359123364 | ||
|
|
cc5357a31a | ||
|
|
f1b60f76eb | ||
|
|
f29e152619 | ||
|
|
92906a500a | ||
|
|
257bd89733 | ||
|
|
1d99bb908d | ||
|
|
591b91194a | ||
|
|
2b2c831253 | ||
|
|
08777100b5 | ||
|
|
a482160691 | ||
|
|
89dd114da1 | ||
|
|
4c05ef48a7 | ||
|
|
14c89c9b63 |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -18,6 +18,12 @@ updates:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: dev
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/docker/tensorrt"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: dev
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/web"
|
||||
schedule:
|
||||
|
||||
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -1,6 +1,7 @@
|
||||
name: On release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -30,34 +31,7 @@ jobs:
|
||||
run: |
|
||||
VERSION_TAG=${BASE}:${CLEAN_VERSION}
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}
|
||||
docker pull ${PULL_TAG}
|
||||
docker tag ${PULL_TAG} ${VERSION_TAG}
|
||||
docker push ${VERSION_TAG}
|
||||
- name: Tag and push standard arm64
|
||||
run: |
|
||||
VERSION_TAG=${BASE}:${CLEAN_VERSION}-standard-arm64
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}-standard-arm64
|
||||
docker pull ${PULL_TAG}
|
||||
docker tag ${PULL_TAG} ${VERSION_TAG}
|
||||
docker push ${VERSION_TAG}
|
||||
- name: Tag and push tensorrt
|
||||
run: |
|
||||
VERSION_TAG=${BASE}:${CLEAN_VERSION}-tensorrt
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}-tensorrt
|
||||
docker pull ${PULL_TAG}
|
||||
docker tag ${PULL_TAG} ${VERSION_TAG}
|
||||
docker push ${VERSION_TAG}
|
||||
- name: Tag and push tensorrt-jp4
|
||||
run: |
|
||||
VERSION_TAG=${BASE}:${CLEAN_VERSION}-tensorrt-jp4
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}-tensorrt-jp4
|
||||
docker pull ${PULL_TAG}
|
||||
docker tag ${PULL_TAG} ${VERSION_TAG}
|
||||
docker push ${VERSION_TAG}
|
||||
- name: Tag and push tensorrt-jp5
|
||||
run: |
|
||||
VERSION_TAG=${BASE}:${CLEAN_VERSION}-tensorrt-jp5
|
||||
PULL_TAG=${BASE}:${BUILD_TAG}-tensorrt-jp5
|
||||
docker pull ${PULL_TAG}
|
||||
docker tag ${PULL_TAG} ${VERSION_TAG}
|
||||
docker push ${VERSION_TAG}
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG}
|
||||
for variant in standard-amd64 tensorrt tensorrt-jp4 tensorrt-jp5 rk; do
|
||||
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant}
|
||||
done
|
||||
|
||||
@@ -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.8.1/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.8.2/go2rtc_linux_${TARGETARCH}" go2rtc
|
||||
|
||||
|
||||
####
|
||||
|
||||
@@ -58,7 +58,15 @@ if go2rtc_config.get("log") is None:
|
||||
elif go2rtc_config["log"].get("format") is None:
|
||||
go2rtc_config["log"]["format"] = "text"
|
||||
|
||||
if not go2rtc_config.get("webrtc", {}).get("candidates", []):
|
||||
# ensure there is a default webrtc config
|
||||
if not go2rtc_config.get("webrtc"):
|
||||
go2rtc_config["webrtc"] = {}
|
||||
|
||||
# go2rtc should listen on 8555 tcp & udp by default
|
||||
if not go2rtc_config["webrtc"].get("listen"):
|
||||
go2rtc_config["webrtc"]["listen"] = ":8555"
|
||||
|
||||
if not go2rtc_config["webrtc"].get("candidates", []):
|
||||
default_candidates = []
|
||||
# use internal candidate if it was discovered when running through the add-on
|
||||
internal_candidate = os.environ.get(
|
||||
|
||||
@@ -34,6 +34,11 @@ http {
|
||||
|
||||
proxy_cache_path /dev/shm/nginx_cache levels=1:2 keys_zone=api_cache:10m max_size=10m inactive=1m use_temp_path=off;
|
||||
|
||||
map $sent_http_content_type $should_not_cache {
|
||||
'application/json' 0;
|
||||
default 1;
|
||||
}
|
||||
|
||||
upstream frigate_api {
|
||||
server 127.0.0.1:5001;
|
||||
keepalive 1024;
|
||||
@@ -192,6 +197,7 @@ http {
|
||||
proxy_cache_use_stale updating;
|
||||
proxy_cache_valid 200 5s;
|
||||
proxy_cache_bypass $http_x_cache_bypass;
|
||||
proxy_no_cache $should_not_cache;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
|
||||
location /api/vod/ {
|
||||
@@ -255,4 +261,4 @@ rtmp {
|
||||
meta copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.8.1, there may be certain cases where you want to run a different version of go2rtc.
|
||||
Frigate currently includes go2rtc v1.8.2, there may be certain cases where you want to run a different version of go2rtc.
|
||||
|
||||
To do this:
|
||||
|
||||
|
||||
@@ -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.8.1#source-rtsp)
|
||||
[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.8.2#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.
|
||||
|
||||
|
||||
@@ -222,9 +222,9 @@ ffmpeg:
|
||||
# Optional: Detect configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
detect:
|
||||
# Optional: width of the frame for the input with the detect role (default: shown below)
|
||||
# Optional: width of the frame for the input with the detect role (default: use native stream resolution)
|
||||
width: 1280
|
||||
# Optional: height of the frame for the input with the detect role (default: shown below)
|
||||
# Optional: height of the frame for the input with the detect role (default: use native stream resolution)
|
||||
height: 720
|
||||
# Optional: desired fps for your camera for the input with the detect role (default: shown below)
|
||||
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
|
||||
@@ -438,7 +438,7 @@ rtmp:
|
||||
enabled: False
|
||||
|
||||
# Optional: Restream configuration
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.8.1)
|
||||
# Uses https://github.com/AlexxIT/go2rtc (v1.8.2)
|
||||
go2rtc:
|
||||
|
||||
# Optional: jsmpeg stream configuration for WebUI
|
||||
|
||||
@@ -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:
|
||||
|
||||
| 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 |
|
||||
| mse | low | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only |
|
||||
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 |
|
||||
| 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 |
|
||||
| mse | low | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only |
|
||||
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config, doesn't support h.265 |
|
||||
|
||||
### Audio Support
|
||||
|
||||
@@ -104,6 +104,7 @@ If you are having difficulties getting WebRTC to work and you are running Frigat
|
||||
If not running in host mode, port 8555 will need to be mapped for the container:
|
||||
|
||||
docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frigate:
|
||||
@@ -115,4 +116,4 @@ services:
|
||||
|
||||
:::
|
||||
|
||||
See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#module-webrtc) for more information about this.
|
||||
See [go2rtc WebRTC docs](https://github.com/AlexxIT/go2rtc/tree/v1.8.2#module-webrtc) for more information about this.
|
||||
|
||||
103
docs/docs/configuration/motion_detection.md
Normal file
103
docs/docs/configuration/motion_detection.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
id: motion_detection
|
||||
title: Motion Detection
|
||||
---
|
||||
|
||||
# Tuning Motion Detection
|
||||
|
||||
Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.
|
||||
|
||||
Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer.
|
||||
|
||||
## The Goal
|
||||
|
||||
The default motion settings should work well for the majority of cameras, however there are cases where tuning motion detection can lead to better and more optimal results. Each camera has its own environment with different variables that affect motion, this means that the same motion settings will not fit all of your cameras.
|
||||
|
||||
Before tuning motion it is important to understand the goal. In an optimal configuration, motion from people and cars would be detected, but not grass moving, lighting changes, timestamps, etc. If your motion detection is too sensitive, you will experience higher CPU loads and greater false positives from the increased rate of object detection. If it is not sensitive enough, you will miss events.
|
||||
|
||||
## Create Motion Masks
|
||||
|
||||
First, mask areas with regular motion not caused by the objects you want to detect. The best way to find candidates for motion masks is by watching the debug stream with motion boxes enabled. Good use cases for motion masks are timestamps or tree limbs and large bushes that regularly move due to wind. When possible, avoid creating motion masks that would block motion detection for objects you want to track **even if they are in locations where you don't want events**. Motion masks should not be used to avoid detecting objects in specific areas. More details can be found [in the masks docs.](/configuration/masks.md).
|
||||
|
||||
## Prepare For Testing
|
||||
|
||||
The easiest way to tune motion detection is to do it live, have one window / screen open with the frigate debug view and motion boxes enabled with another window / screen open allowing for configuring the motion settings. It is recommended to use Home Assistant or MQTT as they offer live configuration of some motion settings meaning that Frigate does not need to be restarted when values are changed.
|
||||
|
||||
In Home Assistant the `Improve Contrast`, `Contour Area`, and `Threshold` configuration entities are disabled by default but can easily be enabled and used to tune live, otherwise MQTT can be used.
|
||||
|
||||
## Tuning Motion Detection During The Day
|
||||
|
||||
Now that things are set up, find a time to tune that represents normal circumstances. For example, if you tune your motion on a day that is sunny and windy you may find later that the motion settings are not sensitive enough on a cloudy and still day.
|
||||
|
||||
:::note
|
||||
|
||||
Remember that motion detection is just used to determine when object detection should be used. You should aim to have motion detection sensitive enough that you won't miss events from objects you want to detect with object detection. The goal is to prevent object detection from running constantly for every small pixel change in the image. Windy days are still going to result in lots of motion being detected.
|
||||
|
||||
:::
|
||||
|
||||
### Threshold
|
||||
|
||||
The threshold value dictates how much of a change in a pixels luminance is required to be considered motion.
|
||||
|
||||
```yaml
|
||||
# default threshold value
|
||||
motion:
|
||||
# Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
|
||||
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
|
||||
# The value should be between 1 and 255.
|
||||
threshold: 30
|
||||
```
|
||||
|
||||
Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dogs blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion.
|
||||
|
||||
Watching the motion boxes in the debug view, increase the threshold until you only see motion that is visible to the eye. Once this is done, it is important to test and ensure that desired motion is still detected.
|
||||
|
||||
### Contour Area
|
||||
|
||||
```yaml
|
||||
# default contour_area value
|
||||
motion:
|
||||
# Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below)
|
||||
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will
|
||||
# make motion detection more sensitive to smaller moving objects.
|
||||
# As a rule of thumb:
|
||||
# - 10 - high sensitivity
|
||||
# - 30 - medium sensitivity
|
||||
# - 50 - low sensitivity
|
||||
contour_area: 10
|
||||
```
|
||||
|
||||
Once the threshold calculation is run, the pixels that have changed are grouped together. The contour area value is used to decide which groups of changed pixels qualify as motion. Smaller values are more sensitive meaning people that are far away, small animals, etc. are more likely to be detected as motion, but it also means that small changes in shadows, leaves, etc. are detected as motion. Higher values are less sensitive meaning these things won't be detected as motion but with the risk that desired motion won't be detected until closer to the camera.
|
||||
|
||||
Watching the motion boxes in the debug view, adjust the contour area until there are no motion boxes smaller than the smallest you'd expect frigate to detect something moving.
|
||||
|
||||
### Improve Contrast
|
||||
|
||||
At this point if motion is working as desired there is no reason to continue with tuning for the day. If you were unable to find a balance between desired and undesired motion being detected, you can try disabling improve contrast and going back to the threshold and contour area steps.
|
||||
|
||||
## Tuning Motion Detection During The Night
|
||||
|
||||
Once daytime motion detection is tuned, there is a chance that the settings will work well for motion detection during the night as well. If this is the case then the preferred settings can be written to the config file and left alone.
|
||||
|
||||
However, if the preferred day settings do not work well at night it is recommended to use HomeAssistant or some other solution to automate changing the settings. That way completely separate sets of motion settings can be used for optimal day and night motion detection.
|
||||
|
||||
## Tuning For Large Changes In Motion
|
||||
|
||||
```yaml
|
||||
# default lightning_threshold:
|
||||
motion:
|
||||
# Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection
|
||||
# needs to recalibrate. (default: shown below)
|
||||
# Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion.
|
||||
# Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching
|
||||
# a doorbell camera.
|
||||
lightning_threshold: 0.8
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
Some cameras like doorbell cameras may have missed detections when someone walks directly in front of the camera and the lightning_threshold causes motion detection to be re-calibrated. In this case, it may be desirable to increase the `lightning_threshold` to ensure these events are not missed.
|
||||
|
||||
:::
|
||||
|
||||
Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in no motion detection. This is done via the `lightning_threshold` configuration. It is defined as the percentage of the image used to detect lightning or other substantial changes where motion detection needs to recalibrate. Increasing this value will make motion detection more likely to consider lightning or IR mode changes as valid motion. Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera.
|
||||
@@ -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.8.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.8.1#configuration) for more advanced configurations and features.
|
||||
Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.8.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.8.2#configuration) for more advanced configurations and features.
|
||||
|
||||
:::note
|
||||
|
||||
@@ -18,6 +18,7 @@ You can access the go2rtc webUI at `http://frigate_ip:5000/live/webrtc` which ca
|
||||
### Birdseye Restream
|
||||
|
||||
Birdseye RTSP restream can be accessed at `rtsp://<frigate_host>:8554/birdseye`. Enabling the birdseye restream will cause birdseye to run 24/7 which may increase CPU usage somewhat.
|
||||
|
||||
```yaml
|
||||
birdseye:
|
||||
restream: true
|
||||
@@ -32,8 +33,7 @@ go2rtc:
|
||||
rtsp:
|
||||
username: "admin"
|
||||
password: "pass"
|
||||
streams:
|
||||
...
|
||||
streams: ...
|
||||
```
|
||||
|
||||
**NOTE:** This does not apply to localhost requests, there is no need to provide credentials when using the restream as a source for frigate cameras.
|
||||
@@ -138,7 +138,7 @@ cameras:
|
||||
|
||||
## Advanced Restream Configurations
|
||||
|
||||
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.8.1#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.8.2#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.8.1#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.8.2#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.8.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.8.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:
|
||||
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.8.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.8.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:
|
||||
|
||||
```yaml
|
||||
go2rtc:
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = {
|
||||
{
|
||||
type: "link",
|
||||
label: "Go2RTC Configuration Reference",
|
||||
href: "https://github.com/AlexxIT/go2rtc/tree/v1.8.1#configuration",
|
||||
href: "https://github.com/AlexxIT/go2rtc/tree/v1.8.2#configuration",
|
||||
},
|
||||
],
|
||||
Detectors: [
|
||||
@@ -32,6 +32,7 @@ module.exports = {
|
||||
"configuration/cameras",
|
||||
"configuration/record",
|
||||
"configuration/snapshots",
|
||||
"configuration/motion_detection",
|
||||
"configuration/birdseye",
|
||||
"configuration/live",
|
||||
"configuration/restream",
|
||||
|
||||
@@ -279,6 +279,17 @@ class FrigateApp:
|
||||
except PermissionError:
|
||||
logger.error("Unable to write to /config to save DB state")
|
||||
|
||||
def cleanup_timeline_db(db: SqliteExtDatabase) -> None:
|
||||
db.execute_sql(
|
||||
"DELETE FROM timeline WHERE source_id NOT IN (SELECT id FROM event);"
|
||||
)
|
||||
|
||||
try:
|
||||
with open(f"{CONFIG_DIR}/.timeline", "w") as f:
|
||||
f.write(str(datetime.datetime.now().timestamp()))
|
||||
except PermissionError:
|
||||
logger.error("Unable to write to /config to save DB state")
|
||||
|
||||
# Migrate DB location
|
||||
old_db_path = DEFAULT_DB_PATH
|
||||
if not os.path.isfile(self.config.database.path) and os.path.isfile(
|
||||
@@ -294,6 +305,11 @@ class FrigateApp:
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
|
||||
# this is a temporary check to clean up user DB from beta
|
||||
# will be removed before final release
|
||||
if not os.path.exists(f"{CONFIG_DIR}/.timeline"):
|
||||
cleanup_timeline_db(migrate_db)
|
||||
|
||||
# check if vacuum needs to be run
|
||||
if os.path.exists(f"{CONFIG_DIR}/.vacuum"):
|
||||
with open(f"{CONFIG_DIR}/.vacuum") as f:
|
||||
@@ -487,7 +503,9 @@ class FrigateApp:
|
||||
# create or update region grids for each camera
|
||||
for camera in self.config.cameras.values():
|
||||
self.region_grids[camera.name] = get_camera_regions_grid(
|
||||
camera.name, camera.detect
|
||||
camera.name,
|
||||
camera.detect,
|
||||
max(self.config.model.width, self.config.model.height),
|
||||
)
|
||||
|
||||
def start_camera_processors(self) -> None:
|
||||
|
||||
@@ -96,7 +96,11 @@ class Dispatcher:
|
||||
elif topic == REQUEST_REGION_GRID:
|
||||
camera = payload
|
||||
self.camera_metrics[camera]["region_grid_queue"].put(
|
||||
get_camera_regions_grid(camera, self.config.cameras[camera].detect)
|
||||
get_camera_regions_grid(
|
||||
camera,
|
||||
self.config.cameras[camera].detect,
|
||||
max(self.config.model.width, self.config.model.height),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.publish(topic, payload, retain=False)
|
||||
|
||||
@@ -17,6 +17,7 @@ from frigate.const import (
|
||||
ALL_ATTRIBUTE_LABELS,
|
||||
AUDIO_MIN_CONFIDENCE,
|
||||
CACHE_DIR,
|
||||
CACHE_SEGMENT_FORMAT,
|
||||
DEFAULT_DB_PATH,
|
||||
REGEX_CAMERA_NAME,
|
||||
YAML_EXT,
|
||||
@@ -865,7 +866,7 @@ class CameraConfig(FrigateBaseModel):
|
||||
|
||||
ffmpeg_output_args = (
|
||||
record_args
|
||||
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
|
||||
+ [f"{os.path.join(CACHE_DIR, self.name)}@{CACHE_SEGMENT_FORMAT}.mp4"]
|
||||
+ ffmpeg_output_args
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ DRIVER_INTEL_iHD = "iHD"
|
||||
|
||||
# Record Values
|
||||
|
||||
CACHE_SEGMENT_FORMAT = "%Y%m%d%H%M%S%z"
|
||||
MAX_SEGMENT_DURATION = 600
|
||||
MAX_PLAYLIST_SECONDS = 7200 # support 2 hour segments for a single playlist to account for cameras with inconsistent segment times
|
||||
|
||||
|
||||
@@ -293,16 +293,6 @@ class TensorRtDetector(DetectionApi):
|
||||
# raw_detections: Nx7 numpy arrays of
|
||||
# [[x, y, w, h, box_confidence, class_id, class_prob],
|
||||
|
||||
# throw out any detections with negative class IDs
|
||||
valid_detections = []
|
||||
for r in raw_detections:
|
||||
if r[5] >= 0:
|
||||
valid_detections.append(r)
|
||||
else:
|
||||
logger.warning(f"Found TensorRT detection with invalid class id {r}")
|
||||
|
||||
raw_detections = valid_detections
|
||||
|
||||
# Calculate score as box_confidence x class_prob
|
||||
raw_detections[:, 4] = raw_detections[:, 4] * raw_detections[:, 6]
|
||||
# Reorder elements by the score, best on top, remove class_prob
|
||||
|
||||
@@ -115,7 +115,7 @@ def is_healthy():
|
||||
@bp.route("/events/summary")
|
||||
def events_summary():
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
hour_modifier, minute_modifier = get_tz_modifiers(tz_name)
|
||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
|
||||
@@ -149,12 +149,7 @@ def events_summary():
|
||||
Event.camera,
|
||||
Event.label,
|
||||
Event.sub_label,
|
||||
fn.strftime(
|
||||
"%Y-%m-%d",
|
||||
fn.datetime(
|
||||
Event.start_time, "unixepoch", hour_modifier, minute_modifier
|
||||
),
|
||||
),
|
||||
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
|
||||
Event.zones,
|
||||
)
|
||||
)
|
||||
@@ -995,7 +990,7 @@ def events():
|
||||
if time_range != DEFAULT_TIME_RANGE:
|
||||
# get timezone arg to ensure browser times are used
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
hour_modifier, minute_modifier = get_tz_modifiers(tz_name)
|
||||
hour_modifier, minute_modifier, _ = get_tz_modifiers(tz_name)
|
||||
|
||||
times = time_range.split(",")
|
||||
time_after = times[0]
|
||||
@@ -1569,7 +1564,7 @@ def get_recordings_storage_usage():
|
||||
@bp.route("/<camera_name>/recordings/summary")
|
||||
def recordings_summary(camera_name):
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
hour_modifier, minute_modifier = get_tz_modifiers(tz_name)
|
||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||
recording_groups = (
|
||||
Recordings.select(
|
||||
fn.strftime(
|
||||
@@ -1583,22 +1578,8 @@ def recordings_summary(camera_name):
|
||||
fn.SUM(Recordings.objects).alias("objects"),
|
||||
)
|
||||
.where(Recordings.camera == camera_name)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(
|
||||
Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d H",
|
||||
fn.datetime(
|
||||
Recordings.start_time, "unixepoch", hour_modifier, minute_modifier
|
||||
),
|
||||
).desc()
|
||||
)
|
||||
.group_by((Recordings.start_time + seconds_offset).cast("int") / 3600)
|
||||
.order_by(Recordings.start_time.desc())
|
||||
.namedtuples()
|
||||
)
|
||||
|
||||
@@ -1613,14 +1594,7 @@ def recordings_summary(camera_name):
|
||||
fn.COUNT(Event.id).alias("count"),
|
||||
)
|
||||
.where(Event.camera == camera_name, Event.has_clip)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(
|
||||
Event.start_time, "unixepoch", hour_modifier, minute_modifier
|
||||
),
|
||||
),
|
||||
)
|
||||
.group_by((Event.start_time + seconds_offset).cast("int") / 3600)
|
||||
.namedtuples()
|
||||
)
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ from ws4py.server.wsgirefserver import (
|
||||
WSGIServer,
|
||||
)
|
||||
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
||||
from ws4py.websocket import WebSocket
|
||||
|
||||
from frigate.comms.ws import WebSocket
|
||||
from frigate.config import BirdseyeModeEnum, FrigateConfig
|
||||
from frigate.const import BASE_DIR, BIRDSEYE_PIPE
|
||||
from frigate.types import CameraMetricsTypes
|
||||
@@ -108,9 +108,12 @@ class Canvas:
|
||||
return camera_aspect
|
||||
|
||||
|
||||
class FFMpegConverter:
|
||||
class FFMpegConverter(threading.Thread):
|
||||
def __init__(
|
||||
self,
|
||||
camera: str,
|
||||
input_queue: queue.Queue,
|
||||
stop_event: mp.Event,
|
||||
in_width: int,
|
||||
in_height: int,
|
||||
out_width: int,
|
||||
@@ -118,6 +121,11 @@ class FFMpegConverter:
|
||||
quality: int,
|
||||
birdseye_rtsp: bool = False,
|
||||
):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = f"{camera}_output_converter"
|
||||
self.camera = camera
|
||||
self.input_queue = input_queue
|
||||
self.stop_event = stop_event
|
||||
self.bd_pipe = None
|
||||
|
||||
if birdseye_rtsp:
|
||||
@@ -167,7 +175,7 @@ class FFMpegConverter:
|
||||
os.close(stdin)
|
||||
self.reading_birdseye = False
|
||||
|
||||
def write(self, b) -> None:
|
||||
def __write(self, b) -> None:
|
||||
self.process.stdin.write(b)
|
||||
|
||||
if self.bd_pipe:
|
||||
@@ -203,9 +211,25 @@ class FFMpegConverter:
|
||||
self.process.kill()
|
||||
self.process.communicate()
|
||||
|
||||
def run(self) -> None:
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
frame = self.input_queue.get(True, timeout=1)
|
||||
self.__write(frame)
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
self.exit()
|
||||
|
||||
|
||||
class BroadcastThread(threading.Thread):
|
||||
def __init__(self, camera, converter, websocket_server, stop_event):
|
||||
def __init__(
|
||||
self,
|
||||
camera: str,
|
||||
converter: FFMpegConverter,
|
||||
websocket_server,
|
||||
stop_event: mp.Event,
|
||||
):
|
||||
super(BroadcastThread, self).__init__()
|
||||
self.camera = camera
|
||||
self.converter = converter
|
||||
@@ -678,15 +702,20 @@ def output_frames(
|
||||
websocket_server.initialize_websockets_manager()
|
||||
websocket_thread = threading.Thread(target=websocket_server.serve_forever)
|
||||
|
||||
inputs: dict[str, queue.Queue] = {}
|
||||
converters = {}
|
||||
broadcasters = {}
|
||||
|
||||
for camera, cam_config in config.cameras.items():
|
||||
inputs[camera] = queue.Queue(maxsize=cam_config.detect.fps)
|
||||
width = int(
|
||||
cam_config.live.height
|
||||
* (cam_config.frame_shape[1] / cam_config.frame_shape[0])
|
||||
)
|
||||
converters[camera] = FFMpegConverter(
|
||||
camera,
|
||||
inputs[camera],
|
||||
stop_event,
|
||||
cam_config.frame_shape[1],
|
||||
cam_config.frame_shape[0],
|
||||
width,
|
||||
@@ -698,7 +727,11 @@ def output_frames(
|
||||
)
|
||||
|
||||
if config.birdseye.enabled:
|
||||
inputs["birdseye"] = queue.Queue(maxsize=10)
|
||||
converters["birdseye"] = FFMpegConverter(
|
||||
"birdseye",
|
||||
inputs["birdseye"],
|
||||
stop_event,
|
||||
config.birdseye.width,
|
||||
config.birdseye.height,
|
||||
config.birdseye.width,
|
||||
@@ -715,6 +748,9 @@ def output_frames(
|
||||
|
||||
websocket_thread.start()
|
||||
|
||||
for t in converters.values():
|
||||
t.start()
|
||||
|
||||
for t in broadcasters.values():
|
||||
t.start()
|
||||
|
||||
@@ -749,7 +785,11 @@ def output_frames(
|
||||
ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager
|
||||
):
|
||||
# write to the converter for the camera if clients are listening to the specific camera
|
||||
converters[camera].write(frame.tobytes())
|
||||
try:
|
||||
inputs[camera].put_nowait(frame.tobytes())
|
||||
except queue.Full:
|
||||
# drop frames if queue is full
|
||||
pass
|
||||
|
||||
if config.birdseye.enabled and (
|
||||
config.birdseye.restream
|
||||
@@ -770,7 +810,11 @@ def output_frames(
|
||||
if config.birdseye.restream:
|
||||
birdseye_buffer[:] = frame_bytes
|
||||
|
||||
converters["birdseye"].write(frame_bytes)
|
||||
try:
|
||||
inputs["birdseye"].put_nowait(frame_bytes)
|
||||
except queue.Full:
|
||||
# drop frames if queue is full
|
||||
pass
|
||||
|
||||
if camera in previous_frames:
|
||||
frame_manager.delete(f"{camera}{previous_frames[camera]}")
|
||||
@@ -790,10 +834,9 @@ def output_frames(
|
||||
frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv)
|
||||
frame_manager.delete(frame_id)
|
||||
|
||||
for c in converters.values():
|
||||
c.exit()
|
||||
for b in broadcasters.values():
|
||||
b.join()
|
||||
|
||||
websocket_server.manager.close_all()
|
||||
websocket_server.manager.stop()
|
||||
websocket_server.manager.join()
|
||||
|
||||
@@ -863,21 +863,11 @@ class PtzAutoTracker:
|
||||
# introduce some hysteresis to prevent a yo-yo zooming effect
|
||||
zoom_out_hysteresis = (
|
||||
self.tracked_object_metrics[camera]["target_box"]
|
||||
> (
|
||||
self.tracked_object_metrics[camera]["original_target_box"]
|
||||
* AUTOTRACKING_ZOOM_OUT_HYSTERESIS
|
||||
)
|
||||
or self.tracked_object_metrics[camera]["target_box"]
|
||||
> self.tracked_object_metrics[camera]["max_target_box"]
|
||||
* AUTOTRACKING_ZOOM_OUT_HYSTERESIS
|
||||
)
|
||||
zoom_in_hysteresis = (
|
||||
self.tracked_object_metrics[camera]["target_box"]
|
||||
< (
|
||||
self.tracked_object_metrics[camera]["original_target_box"]
|
||||
* AUTOTRACKING_ZOOM_IN_HYSTERESIS
|
||||
)
|
||||
or self.tracked_object_metrics[camera]["target_box"]
|
||||
< self.tracked_object_metrics[camera]["max_target_box"]
|
||||
* AUTOTRACKING_ZOOM_IN_HYSTERESIS
|
||||
)
|
||||
@@ -1073,15 +1063,14 @@ class PtzAutoTracker:
|
||||
)
|
||||
) is not None:
|
||||
# zoom value
|
||||
limit = (
|
||||
self.tracked_object_metrics[camera]["original_target_box"]
|
||||
if self.tracked_object_metrics[camera]["target_box"]
|
||||
< self.tracked_object_metrics[camera]["max_target_box"]
|
||||
else self.tracked_object_metrics[camera]["max_target_box"]
|
||||
ratio = (
|
||||
self.tracked_object_metrics[camera]["max_target_box"]
|
||||
/ self.tracked_object_metrics[camera]["target_box"]
|
||||
)
|
||||
ratio = limit / self.tracked_object_metrics[camera]["target_box"]
|
||||
zoom = (ratio - 1) / (ratio + 1)
|
||||
logger.debug(f"{camera}: Zoom calculation: {zoom}")
|
||||
logger.debug(
|
||||
f'{camera}: limit: {self.tracked_object_metrics[camera]["max_target_box"]}, ratio: {ratio} zoom calculation: {zoom}'
|
||||
)
|
||||
if not result:
|
||||
# zoom out with special condition if zooming out because of velocity, edges, etc.
|
||||
zoom = -(1 - zoom) if zoom > 0 else -(zoom * 2 + 1)
|
||||
|
||||
@@ -11,7 +11,7 @@ from frigate.config import FrigateConfig, RetainModeEnum
|
||||
from frigate.const import CACHE_DIR, RECORD_DIR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.record.util import remove_empty_directories, sync_recordings
|
||||
from frigate.util.builtin import get_tomorrow_at_time
|
||||
from frigate.util.builtin import clear_and_unlink, get_tomorrow_at_time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,11 +31,7 @@ class RecordingCleanup(threading.Thread):
|
||||
logger.debug(f"Checking tmp clip {p}.")
|
||||
if p.stat().st_mtime < (datetime.datetime.now().timestamp() - 60 * 1):
|
||||
logger.debug("Deleting tmp clip.")
|
||||
|
||||
# empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769
|
||||
with open(p, "w"):
|
||||
pass
|
||||
p.unlink(missing_ok=True)
|
||||
clear_and_unlink(p)
|
||||
|
||||
def expire_recordings(self) -> None:
|
||||
"""Delete recordings based on retention config."""
|
||||
|
||||
@@ -20,6 +20,7 @@ import psutil
|
||||
from frigate.config import FrigateConfig, RetainModeEnum
|
||||
from frigate.const import (
|
||||
CACHE_DIR,
|
||||
CACHE_SEGMENT_FORMAT,
|
||||
INSERT_MANY_RECORDINGS,
|
||||
MAX_SEGMENT_DURATION,
|
||||
RECORD_DIR,
|
||||
@@ -31,6 +32,8 @@ from frigate.util.services import get_video_properties
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
QUEUE_READ_TIMEOUT = 0.00001 # seconds
|
||||
|
||||
|
||||
class SegmentInfo:
|
||||
def __init__(
|
||||
@@ -74,15 +77,13 @@ class RecordingMaintainer(threading.Thread):
|
||||
self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {}
|
||||
|
||||
async def move_files(self) -> None:
|
||||
cache_files = sorted(
|
||||
[
|
||||
d
|
||||
for d in os.listdir(CACHE_DIR)
|
||||
if os.path.isfile(os.path.join(CACHE_DIR, d))
|
||||
and d.endswith(".mp4")
|
||||
and not d.startswith("clip_")
|
||||
]
|
||||
)
|
||||
cache_files = [
|
||||
d
|
||||
for d in os.listdir(CACHE_DIR)
|
||||
if os.path.isfile(os.path.join(CACHE_DIR, d))
|
||||
and d.endswith(".mp4")
|
||||
and not d.startswith("clip_")
|
||||
]
|
||||
|
||||
files_in_use = []
|
||||
for process in psutil.process_iter():
|
||||
@@ -106,8 +107,12 @@ class RecordingMaintainer(threading.Thread):
|
||||
|
||||
cache_path = os.path.join(CACHE_DIR, cache)
|
||||
basename = os.path.splitext(cache)[0]
|
||||
camera, date = basename.rsplit("-", maxsplit=1)
|
||||
start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
|
||||
camera, date = basename.rsplit("@", maxsplit=1)
|
||||
|
||||
# important that start_time is utc because recordings are stored and compared in utc
|
||||
start_time = datetime.datetime.strptime(
|
||||
date, CACHE_SEGMENT_FORMAT
|
||||
).astimezone(datetime.timezone.utc)
|
||||
|
||||
grouped_recordings[camera].append(
|
||||
{
|
||||
@@ -119,6 +124,11 @@ class RecordingMaintainer(threading.Thread):
|
||||
# delete all cached files past the most recent 5
|
||||
keep_count = 5
|
||||
for camera in grouped_recordings.keys():
|
||||
# sort based on start time
|
||||
grouped_recordings[camera] = sorted(
|
||||
grouped_recordings[camera], key=lambda s: s["start_time"]
|
||||
)
|
||||
|
||||
segment_count = len(grouped_recordings[camera])
|
||||
if segment_count > keep_count:
|
||||
logger.warning(
|
||||
@@ -163,8 +173,6 @@ class RecordingMaintainer(threading.Thread):
|
||||
Event.has_clip,
|
||||
)
|
||||
.order_by(Event.start_time)
|
||||
.namedtuples()
|
||||
.iterator()
|
||||
)
|
||||
|
||||
tasks.extend(
|
||||
@@ -218,7 +226,7 @@ class RecordingMaintainer(threading.Thread):
|
||||
# if cached file's start_time is earlier than the retain days for the camera
|
||||
if start_time <= (
|
||||
(
|
||||
datetime.datetime.now()
|
||||
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
||||
- datetime.timedelta(
|
||||
days=self.config.cameras[camera].record.retain.days
|
||||
)
|
||||
@@ -263,7 +271,7 @@ class RecordingMaintainer(threading.Thread):
|
||||
retain_cutoff = datetime.datetime.fromtimestamp(
|
||||
most_recently_processed_frame_time - pre_capture
|
||||
).astimezone(datetime.timezone.utc)
|
||||
if end_time.astimezone(datetime.timezone.utc) < retain_cutoff:
|
||||
if end_time < retain_cutoff:
|
||||
Path(cache_path).unlink(missing_ok=True)
|
||||
self.end_time_cache.pop(cache_path, None)
|
||||
# else retain days includes this segment
|
||||
@@ -275,10 +283,11 @@ class RecordingMaintainer(threading.Thread):
|
||||
)
|
||||
|
||||
# ensure delayed segment info does not lead to lost segments
|
||||
if datetime.datetime.fromtimestamp(
|
||||
most_recently_processed_frame_time
|
||||
).astimezone(datetime.timezone.utc) >= end_time.astimezone(
|
||||
datetime.timezone.utc
|
||||
if (
|
||||
datetime.datetime.fromtimestamp(
|
||||
most_recently_processed_frame_time
|
||||
).astimezone(datetime.timezone.utc)
|
||||
>= end_time
|
||||
):
|
||||
record_mode = self.config.cameras[camera].record.retain.mode
|
||||
return await self.move_segment(
|
||||
@@ -345,18 +354,18 @@ class RecordingMaintainer(threading.Thread):
|
||||
self.end_time_cache.pop(cache_path, None)
|
||||
return
|
||||
|
||||
# directory will be in utc due to start_time being in utc
|
||||
directory = os.path.join(
|
||||
RECORD_DIR,
|
||||
start_time.astimezone(tz=datetime.timezone.utc).strftime("%Y-%m-%d/%H"),
|
||||
start_time.strftime("%Y-%m-%d/%H"),
|
||||
camera,
|
||||
)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
file_name = (
|
||||
f"{start_time.replace(tzinfo=datetime.timezone.utc).strftime('%M.%S.mp4')}"
|
||||
)
|
||||
# file will be in utc due to start_time being in utc
|
||||
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
try:
|
||||
@@ -443,7 +452,9 @@ class RecordingMaintainer(threading.Thread):
|
||||
current_tracked_objects,
|
||||
motion_boxes,
|
||||
regions,
|
||||
) = self.object_recordings_info_queue.get(True, timeout=0.01)
|
||||
) = self.object_recordings_info_queue.get(
|
||||
True, timeout=QUEUE_READ_TIMEOUT
|
||||
)
|
||||
|
||||
if frame_time < run_start - stale_frame_count_threshold:
|
||||
stale_frame_count += 1
|
||||
@@ -479,7 +490,9 @@ class RecordingMaintainer(threading.Thread):
|
||||
frame_time,
|
||||
dBFS,
|
||||
audio_detections,
|
||||
) = self.audio_recordings_info_queue.get(True, timeout=0.01)
|
||||
) = self.audio_recordings_info_queue.get(
|
||||
True, timeout=QUEUE_READ_TIMEOUT
|
||||
)
|
||||
|
||||
if frame_time < run_start - stale_frame_count_threshold:
|
||||
stale_frame_count += 1
|
||||
|
||||
@@ -108,15 +108,12 @@ def sync_recordings(limited: bool) -> None:
|
||||
|
||||
if limited:
|
||||
# get recording files from last 36 hours
|
||||
hour_check = (
|
||||
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
||||
- datetime.timedelta(hours=36)
|
||||
).strftime("%Y-%m-%d/%H")
|
||||
hour_check = f"{RECORD_DIR}/{(datetime.datetime.now().astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36)).strftime('%Y-%m-%d/%H')}"
|
||||
files_on_disk = {
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(RECORD_DIR)
|
||||
for file in files
|
||||
if file > hour_check
|
||||
if root > hour_check
|
||||
}
|
||||
else:
|
||||
# get all recordings files on disk and put them in a set
|
||||
|
||||
@@ -10,6 +10,7 @@ from peewee import fn
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import RECORD_DIR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.util.builtin import clear_and_unlink
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
bandwidth_equation = Recordings.segment_size / (
|
||||
@@ -35,7 +36,7 @@ class StorageMaintainer(threading.Thread):
|
||||
if self.camera_storage_stats.get(camera, {}).get("needs_refresh", True):
|
||||
self.camera_storage_stats[camera] = {
|
||||
"needs_refresh": (
|
||||
Recordings.select(fn.COUNT(Recordings.id))
|
||||
Recordings.select(fn.COUNT("*"))
|
||||
.where(Recordings.camera == camera, Recordings.segment_size > 0)
|
||||
.scalar()
|
||||
< 50
|
||||
@@ -160,7 +161,7 @@ class StorageMaintainer(threading.Thread):
|
||||
# Delete recordings not retained indefinitely
|
||||
if not keep:
|
||||
try:
|
||||
Path(recording.path).unlink(missing_ok=False)
|
||||
clear_and_unlink(Path(recording.path), missing_ok=False)
|
||||
deleted_recordings.add(recording.id)
|
||||
deleted_segments_size += recording.segment_size
|
||||
except FileNotFoundError:
|
||||
@@ -188,7 +189,7 @@ class StorageMaintainer(threading.Thread):
|
||||
break
|
||||
|
||||
try:
|
||||
Path(recording.path).unlink(missing_ok=False)
|
||||
clear_and_unlink(Path(recording.path), missing_ok=False)
|
||||
deleted_segments_size += recording.segment_size
|
||||
deleted_recordings.add(recording.id)
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -102,5 +102,11 @@ class TimelineProcessor(threading.Thread):
|
||||
)[0]
|
||||
Timeline.insert(timeline_entry).execute()
|
||||
elif event_type == "end":
|
||||
timeline_entry[Timeline.class_type] = "gone"
|
||||
Timeline.insert(timeline_entry).execute()
|
||||
if event_data["has_clip"] or event_data["has_snapshot"]:
|
||||
timeline_entry[Timeline.class_type] = "gone"
|
||||
Timeline.insert(timeline_entry).execute()
|
||||
else:
|
||||
# if event was not saved then the timeline entries should be deleted
|
||||
Timeline.delete().where(
|
||||
Timeline.source_id == event_data["id"]
|
||||
).execute()
|
||||
|
||||
@@ -8,6 +8,7 @@ import shlex
|
||||
import urllib.parse
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any, Tuple
|
||||
|
||||
import numpy as np
|
||||
@@ -156,7 +157,7 @@ def load_labels(path, encoding="utf-8", prefill=91):
|
||||
return labels
|
||||
|
||||
|
||||
def get_tz_modifiers(tz_name: str) -> Tuple[str, str]:
|
||||
def get_tz_modifiers(tz_name: str) -> Tuple[str, str, int]:
|
||||
seconds_offset = (
|
||||
datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds()
|
||||
)
|
||||
@@ -164,7 +165,7 @@ def get_tz_modifiers(tz_name: str) -> Tuple[str, str]:
|
||||
minutes_offset = int(seconds_offset / 60 - hours_offset * 60)
|
||||
hour_modifier = f"{hours_offset} hour"
|
||||
minute_modifier = f"{minutes_offset} minute"
|
||||
return hour_modifier, minute_modifier
|
||||
return hour_modifier, minute_modifier, seconds_offset
|
||||
|
||||
|
||||
def to_relative_box(
|
||||
@@ -269,3 +270,15 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime:
|
||||
return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(
|
||||
datetime.timezone.utc
|
||||
)
|
||||
|
||||
|
||||
def clear_and_unlink(file: Path, missing_ok: bool = True) -> None:
|
||||
"""clear file then unlink to avoid space retained by file descriptors."""
|
||||
if not missing_ok and not file.exists():
|
||||
raise FileNotFoundError()
|
||||
|
||||
# empty contents of file before unlinking https://github.com/blakeblackshear/frigate/issues/4769
|
||||
with open(file, "w"):
|
||||
pass
|
||||
|
||||
file.unlink(missing_ok=missing_ok)
|
||||
|
||||
@@ -30,7 +30,9 @@ GRID_SIZE = 8
|
||||
|
||||
|
||||
def get_camera_regions_grid(
|
||||
name: str, detect: DetectConfig
|
||||
name: str,
|
||||
detect: DetectConfig,
|
||||
min_region_size: int,
|
||||
) -> list[list[dict[str, any]]]:
|
||||
"""Build a grid of expected region sizes for a camera."""
|
||||
# get grid from db if available
|
||||
@@ -99,7 +101,7 @@ def get_camera_regions_grid(
|
||||
box[1] * height,
|
||||
(box[0] + box[2]) * width,
|
||||
(box[1] + box[3]) * height,
|
||||
320,
|
||||
min_region_size,
|
||||
1.35,
|
||||
)
|
||||
# save width of region to grid as relative
|
||||
|
||||
@@ -16,6 +16,7 @@ from frigate.const import (
|
||||
ALL_ATTRIBUTE_LABELS,
|
||||
ATTRIBUTE_LABEL_MAP,
|
||||
CACHE_DIR,
|
||||
CACHE_SEGMENT_FORMAT,
|
||||
REQUEST_REGION_GRID,
|
||||
)
|
||||
from frigate.log import LogPipe
|
||||
@@ -300,19 +301,19 @@ class CameraWatchdog(threading.Thread):
|
||||
and not d.startswith("clip_")
|
||||
]
|
||||
)
|
||||
newest_segment_timestamp = latest_segment
|
||||
newest_segment_time = latest_segment
|
||||
|
||||
for file in cache_files:
|
||||
if self.camera_name in file:
|
||||
basename = os.path.splitext(file)[0]
|
||||
_, date = basename.rsplit("-", maxsplit=1)
|
||||
ts = datetime.datetime.strptime(date, "%Y%m%d%H%M%S").astimezone(
|
||||
datetime.timezone.utc
|
||||
)
|
||||
if ts > newest_segment_timestamp:
|
||||
newest_segment_timestamp = ts
|
||||
_, date = basename.rsplit("@", maxsplit=1)
|
||||
segment_time = datetime.datetime.strptime(
|
||||
date, CACHE_SEGMENT_FORMAT
|
||||
).astimezone(datetime.timezone.utc)
|
||||
if segment_time > newest_segment_time:
|
||||
newest_segment_time = segment_time
|
||||
|
||||
return newest_segment_timestamp
|
||||
return newest_segment_time
|
||||
|
||||
|
||||
class CameraCapture(threading.Thread):
|
||||
|
||||
40
migrations/020_update_index_recordings.py
Normal file
40
migrations/020_update_index_recordings.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Peewee migrations -- 020_update_index_recordings.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['model_name'] # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.python(func, *args, **kwargs) # Run python code
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
|
||||
"""
|
||||
import peewee as pw
|
||||
|
||||
SQL = pw.SQL
|
||||
|
||||
|
||||
def migrate(migrator, database, fake=False, **kwargs):
|
||||
migrator.sql("DROP INDEX recordings_end_time_start_time")
|
||||
migrator.sql(
|
||||
'CREATE INDEX "recordings_camera_start_time_end_time" ON "recordings" ("camera", "start_time" DESC, "end_time" DESC)'
|
||||
)
|
||||
migrator.sql(
|
||||
'CREATE INDEX "recordings_api_recordings_summary" ON "recordings" ("camera", "start_time" DESC, "duration", "motion", "objects")'
|
||||
)
|
||||
migrator.sql('CREATE INDEX "recordings_start_time" ON "recordings" ("start_time")')
|
||||
|
||||
|
||||
def rollback(migrator, database, fake=False, **kwargs):
|
||||
pass
|
||||
@@ -40,7 +40,7 @@ export default function AppBar() {
|
||||
setShowDialog(false);
|
||||
setShowDialogWait(true);
|
||||
sendRestart();
|
||||
}, [setShowDialog]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [setShowDialog, sendRestart]);
|
||||
|
||||
const handleDismissRestartDialog = useCallback(() => {
|
||||
setShowDialog(false);
|
||||
|
||||
@@ -353,6 +353,7 @@ ${Object.keys(objectMaskPoints)
|
||||
snap={snap}
|
||||
width={width}
|
||||
height={height}
|
||||
setError={setError}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-xs">
|
||||
@@ -434,7 +435,7 @@ function boundedSize(value, maxValue, snap) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
function EditableMask({ onChange, points, scale, snap, width, height, setError }) {
|
||||
const boundingRef = useRef(null);
|
||||
|
||||
const handleMovePoint = useCallback(
|
||||
@@ -455,6 +456,11 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
// Add a new point between the closest two other points
|
||||
const handleAddPoint = useCallback(
|
||||
(event) => {
|
||||
if (!points) {
|
||||
setError('You must choose an item to edit or add a new item before adding a point.');
|
||||
return
|
||||
}
|
||||
|
||||
const { offsetX, offsetY } = event;
|
||||
const scaledX = boundedSize((offsetX - MaskInset) / scale, width, snap);
|
||||
const scaledY = boundedSize((offsetY - MaskInset) / scale, height, snap);
|
||||
@@ -474,7 +480,7 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||
newPoints.splice(index, 0, newPoint);
|
||||
onChange(newPoints);
|
||||
},
|
||||
[height, width, scale, points, onChange, snap]
|
||||
[height, width, scale, points, onChange, snap, setError]
|
||||
);
|
||||
|
||||
const handleRemovePoint = useCallback(
|
||||
|
||||
@@ -13,6 +13,12 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000'
|
||||
},
|
||||
'/vod': {
|
||||
target: 'http://localhost:5000'
|
||||
},
|
||||
'/exports': {
|
||||
target: 'http://localhost:5000'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user