Compare commits

..

24 Commits

Author SHA1 Message Date
Nicolas Mowen
aa93d4fbdd Fix nginx cache (#8558) 2023-11-09 16:09:25 -06:00
Nicolas Mowen
d0036b2f77 Update go2rtc to 1.8.2 (#8459)
* Update go2rtc to 1.8.2

* Update docs for 1.8.2
2023-11-09 06:43:06 -06:00
Nicolas Mowen
1b57f8c7e2 Show error when clicking on image before mask (#8547) 2023-11-09 06:42:19 -06:00
Nicolas Mowen
fa96ec64e4 Detect Resolution Docs (#8542) 2023-11-09 06:41:50 -06:00
Josh Hawkins
e89db13282 fix restart from webui (#8541) 2023-11-08 09:37:54 -07:00
Nicolas Mowen
fe6577736e Tuning Motion guide (#6877)
* start working on motion guide

* Update motion_detection.md

* Add to sidebar

* Add title / ID

* Fix typos

* Fix typos and clarify

* Formatting

* Add default values from reference config for each section

* Small grammar fix

* Move to cameras section

* Updates

* Clarify and improve wording

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>

---------

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2023-11-08 14:04:27 +00:00
Blake Blackshear
64537672e6 use skopeo to copy images (#8539) 2023-11-08 07:22:23 -06:00
Nicolas Mowen
ef36aabd30 Make output writing asynchronous (#8530)
* Don't wait for frame write in output process

* Formatting

* Handle websocket exception

* Limit camera queeue size to 1 second
2023-11-07 16:24:56 -07:00
Nicolas Mowen
ca84732574 Ensure storage cleanup doesn't fail (#8531) 2023-11-07 23:21:43 +00:00
Nicolas Mowen
0b828ef1ec Only cache json requests with nginx (#8529)
* Only cache json requests

* Use map instead
2023-11-07 23:18:58 +00:00
tpjanssen
3359123364 Performance increase with lots of recordings (#8525)
* Performance increase with lots of recordings

* Formatting + review

* Update 020_update_index_recordings.py

Formatting

* Feedback + check fix

* Update 020_update_index_recordings.py
2023-11-07 23:18:26 +00:00
Nicolas Mowen
cc5357a31a Remove iterator from recordings events (#8522) 2023-11-07 07:02:02 -07:00
Nicolas Mowen
f1b60f76eb Fix go2rtc UDP port default config (#8469)
* Fix go2rtc UDP port

* fix

* Use correct port

* fix comment

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>

---------

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2023-11-07 11:33:33 +00:00
Nicolas Mowen
f29e152619 Revert TRT class id filtering (#8497) 2023-11-07 11:32:52 +00:00
Nicolas Mowen
92906a500a Fix event only datetime check (#8500) 2023-11-07 11:32:31 +00:00
Nicolas Mowen
257bd89733 Reduce recording info timeout (#8506)
* Decrease timeout

* Use const for queue timeout
2023-11-07 11:32:11 +00:00
Josh Hawkins
1d99bb908d only use max target box for relative zoom limiter (#8509) 2023-11-07 11:31:39 +00:00
Nicolas Mowen
591b91194a Cleanup existing timeline entries (#8495)
* Cleanup existing timeline entries for beta users

* Formatting
2023-11-06 06:43:26 -07:00
Nicolas Mowen
2b2c831253 Set the min region size as the model size (#8486) 2023-11-06 06:45:37 -06:00
tpjanssen
08777100b5 Add /vod and /exports to Vite proxy config (#8490) 2023-11-06 06:44:53 -06:00
Nicolas Mowen
a482160691 Cleanup timeline entries (#8489) 2023-11-06 06:42:10 -06:00
Nicolas Mowen
89dd114da1 Write recording segments to cache with timezone info (#8468)
* Store recording segments with timezone info

* Don't use _

* Use different separator due to timezone
2023-11-05 14:30:29 -06:00
Nicolas Mowen
4c05ef48a7 Add dependabot to tensorrt python deps (#8455) 2023-11-05 14:29:06 -06:00
Nicolas Mowen
14c89c9b63 Fix periodic sync check (#8472)
* Fix periodic sync

* Formatting
2023-11-05 09:28:40 -07:00
33 changed files with 379 additions and 179 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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
####

View File

@@ -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(

View File

@@ -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;
}
}
}
}

View File

@@ -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:

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View 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.

View File

@@ -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}}`

View File

@@ -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:

View File

@@ -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",

View File

@@ -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:

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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()
)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View 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

View File

@@ -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);

View File

@@ -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(

View File

@@ -13,6 +13,12 @@ export default defineConfig({
proxy: {
'/api': {
target: 'http://localhost:5000'
},
'/vod': {
target: 'http://localhost:5000'
},
'/exports': {
target: 'http://localhost:5000'
}
}
},