mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-23 16:48:23 +03:00
Merge remote-tracking branch 'origin/master' into dev
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
This commit is contained in:
commit
59f570f436
@ -55,7 +55,7 @@ function setup_homekit_config() {
|
|||||||
|
|
||||||
if [[ ! -f "${config_path}" ]]; then
|
if [[ ! -f "${config_path}" ]]; then
|
||||||
echo "[INFO] Creating empty config file for HomeKit..."
|
echo "[INFO] Creating empty config file for HomeKit..."
|
||||||
echo '{}' > "${config_path}"
|
: > "${config_path}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Convert YAML to JSON for jq processing
|
# Convert YAML to JSON for jq processing
|
||||||
@ -65,23 +65,25 @@ function setup_homekit_config() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Use jq to filter and keep only the homekit section
|
# Use jq to extract the homekit section, if it exists
|
||||||
local cleaned_json="/tmp/cache/homekit_cleaned.json"
|
local homekit_json
|
||||||
jq '
|
homekit_json=$(jq '
|
||||||
# Keep only the homekit section if it exists, otherwise empty object
|
if has("homekit") then {homekit: .homekit} else null end
|
||||||
if has("homekit") then {homekit: .homekit} else {} end
|
' "${temp_json}" 2>/dev/null) || homekit_json="null"
|
||||||
' "${temp_json}" > "${cleaned_json}" 2>/dev/null || {
|
|
||||||
echo '{}' > "${cleaned_json}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert back to YAML and write to the config file
|
# If no homekit section, write an empty config file
|
||||||
yq eval -P "${cleaned_json}" > "${config_path}" 2>/dev/null || {
|
if [[ "${homekit_json}" == "null" ]]; then
|
||||||
echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config"
|
: > "${config_path}"
|
||||||
echo '{}' > "${config_path}"
|
else
|
||||||
}
|
# Convert homekit JSON back to YAML and write to the config file
|
||||||
|
echo "${homekit_json}" | yq eval -P - > "${config_path}" 2>/dev/null || {
|
||||||
|
echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config"
|
||||||
|
: > "${config_path}"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# Clean up temp files
|
# Clean up temp files
|
||||||
rm -f "${temp_json}" "${cleaned_json}"
|
rm -f "${temp_json}"
|
||||||
}
|
}
|
||||||
|
|
||||||
set_libva_version
|
set_libva_version
|
||||||
|
|||||||
@ -44,13 +44,21 @@ go2rtc:
|
|||||||
|
|
||||||
### `environment_vars`
|
### `environment_vars`
|
||||||
|
|
||||||
This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS.
|
This section can be used to set environment variables for those unable to modify the environment of the container, like within Home Assistant OS. Docker users should set environment variables in their `docker run` command (`-e FRIGATE_MQTT_PASSWORD=secret`) or `docker-compose.yml` file (`environment:` section) instead. Note that values set here are stored in plain text in your config file, so if the goal is to keep credentials out of your configuration, use Docker environment variables or Docker secrets instead.
|
||||||
|
|
||||||
|
Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment_vars:
|
environment_vars:
|
||||||
VARIABLE_NAME: variable_value
|
FRIGATE_MQTT_USER: my_mqtt_user
|
||||||
|
FRIGATE_MQTT_PASSWORD: my_mqtt_password
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
host: "{FRIGATE_MQTT_HOST}"
|
||||||
|
user: "{FRIGATE_MQTT_USER}"
|
||||||
|
password: "{FRIGATE_MQTT_PASSWORD}"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### TensorFlow Thread Configuration
|
#### TensorFlow Thread Configuration
|
||||||
|
|||||||
@ -86,7 +86,7 @@ Frigate looks for a JWT token secret in the following order:
|
|||||||
|
|
||||||
1. An environment variable named `FRIGATE_JWT_SECRET`
|
1. An environment variable named `FRIGATE_JWT_SECRET`
|
||||||
2. A file named `FRIGATE_JWT_SECRET` in the directory specified by the `CREDENTIALS_DIRECTORY` environment variable (defaults to the Docker Secrets directory: `/run/secrets/`)
|
2. A file named `FRIGATE_JWT_SECRET` in the directory specified by the `CREDENTIALS_DIRECTORY` environment variable (defaults to the Docker Secrets directory: `/run/secrets/`)
|
||||||
3. A `jwt_secret` option from the Home Assistant Add-on options
|
3. A `jwt_secret` option from the Home Assistant App options
|
||||||
4. A `.jwt_secret` file in the config directory
|
4. A `.jwt_secret` file in the config directory
|
||||||
|
|
||||||
If no secret is found on startup, Frigate generates one and stores it in a `.jwt_secret` file in the config directory.
|
If no secret is found on startup, Frigate generates one and stores it in a `.jwt_secret` file in the config directory.
|
||||||
@ -232,7 +232,7 @@ The viewer role provides read-only access to all cameras in the UI and API. Cust
|
|||||||
|
|
||||||
### Role Configuration Example
|
### Role Configuration Example
|
||||||
|
|
||||||
```yaml
|
```yaml {11-16}
|
||||||
cameras:
|
cameras:
|
||||||
front_door:
|
front_door:
|
||||||
# ... camera config
|
# ... camera config
|
||||||
|
|||||||
@ -24,7 +24,7 @@ A custom icon can be added to the birdseye background by providing a 180x180 ima
|
|||||||
|
|
||||||
If you want to include a camera in Birdseye view only for specific circumstances, or just don't include it at all, the Birdseye setting can be set at the camera level.
|
If you want to include a camera in Birdseye view only for specific circumstances, or just don't include it at all, the Birdseye setting can be set at the camera level.
|
||||||
|
|
||||||
```yaml
|
```yaml {8-10,12-14}
|
||||||
# Include all cameras by default in Birdseye view
|
# Include all cameras by default in Birdseye view
|
||||||
birdseye:
|
birdseye:
|
||||||
enabled: True
|
enabled: True
|
||||||
@ -48,6 +48,7 @@ By default birdseye shows all cameras that have had the configured activity in t
|
|||||||
```yaml
|
```yaml
|
||||||
birdseye:
|
birdseye:
|
||||||
enabled: True
|
enabled: True
|
||||||
|
# highlight-next-line
|
||||||
inactivity_threshold: 15
|
inactivity_threshold: 15
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -78,9 +79,11 @@ birdseye:
|
|||||||
cameras:
|
cameras:
|
||||||
front:
|
front:
|
||||||
birdseye:
|
birdseye:
|
||||||
|
# highlight-next-line
|
||||||
order: 1
|
order: 1
|
||||||
back:
|
back:
|
||||||
birdseye:
|
birdseye:
|
||||||
|
# highlight-next-line
|
||||||
order: 2
|
order: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ It is possible to limit the number of cameras shown on birdseye at one time. Whe
|
|||||||
|
|
||||||
For example, this can be configured to only show the most recently active camera.
|
For example, this can be configured to only show the most recently active camera.
|
||||||
|
|
||||||
```yaml
|
```yaml {3-4}
|
||||||
birdseye:
|
birdseye:
|
||||||
enabled: True
|
enabled: True
|
||||||
layout:
|
layout:
|
||||||
@ -103,7 +106,7 @@ birdseye:
|
|||||||
|
|
||||||
By default birdseye tries to fit 2 cameras in each row and then double in size until a suitable layout is found. The scaling can be configured with a value between 1.0 and 5.0 depending on use case.
|
By default birdseye tries to fit 2 cameras in each row and then double in size until a suitable layout is found. The scaling can be configured with a value between 1.0 and 5.0 depending on use case.
|
||||||
|
|
||||||
```yaml
|
```yaml {3-4}
|
||||||
birdseye:
|
birdseye:
|
||||||
enabled: True
|
enabled: True
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
@ -23,6 +23,7 @@ Some cameras support h265 with different formats, but Safari only supports the a
|
|||||||
cameras:
|
cameras:
|
||||||
h265_cam: # <------ Doesn't matter what the camera is called
|
h265_cam: # <------ Doesn't matter what the camera is called
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
|
# highlight-next-line
|
||||||
apple_compatibility: true # <- Adds compatibility with MacOS and iPhone
|
apple_compatibility: true # <- Adds compatibility with MacOS and iPhone
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ cameras:
|
|||||||
|
|
||||||
Note that mjpeg cameras require encoding the video into h264 for recording, and restream roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. It is recommended to use the restream role to create an h264 restream and then use that as the source for ffmpeg.
|
Note that mjpeg cameras require encoding the video into h264 for recording, and restream roles. This will use significantly more CPU than if the cameras supported h264 feeds directly. It is recommended to use the restream role to create an h264 restream and then use that as the source for ffmpeg.
|
||||||
|
|
||||||
```yaml
|
```yaml {3,10}
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
mjpeg_cam: "ffmpeg:http://your_mjpeg_stream_url#video=h264#hardware" # <- use hardware acceleration to create an h264 stream usable for other components.
|
mjpeg_cam: "ffmpeg:http://your_mjpeg_stream_url#video=h264#hardware" # <- use hardware acceleration to create an h264 stream usable for other components.
|
||||||
@ -96,6 +97,7 @@ This camera is H.265 only. To be able to play clips on some devices (like MacOs
|
|||||||
cameras:
|
cameras:
|
||||||
annkec800: # <------ Name the camera
|
annkec800: # <------ Name the camera
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
|
# highlight-next-line
|
||||||
apple_compatibility: true # <- Adds compatibility with MacOS and iPhone
|
apple_compatibility: true # <- Adds compatibility with MacOS and iPhone
|
||||||
output_args:
|
output_args:
|
||||||
record: preset-record-generic-audio-aac
|
record: preset-record-generic-audio-aac
|
||||||
@ -274,7 +276,7 @@ To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's
|
|||||||
|
|
||||||
- In your Frigate Configuration File, add the go2rtc stream and roles as appropriate:
|
- In your Frigate Configuration File, add the go2rtc stream and roles as appropriate:
|
||||||
|
|
||||||
```
|
```yaml {4,11-12}
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
usb_camera:
|
usb_camera:
|
||||||
|
|||||||
@ -66,7 +66,7 @@ Not every PTZ supports ONVIF, which is the standard protocol Frigate uses to com
|
|||||||
|
|
||||||
Add the onvif section to your camera in your configuration file:
|
Add the onvif section to your camera in your configuration file:
|
||||||
|
|
||||||
```yaml
|
```yaml {4-8}
|
||||||
cameras:
|
cameras:
|
||||||
back:
|
back:
|
||||||
ffmpeg: ...
|
ffmpeg: ...
|
||||||
|
|||||||
@ -7,11 +7,11 @@ Object classification allows you to train a custom MobileNetV2 classification mo
|
|||||||
|
|
||||||
## Minimum System Requirements
|
## Minimum System Requirements
|
||||||
|
|
||||||
Object classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate.
|
Object classification models are lightweight and run very fast on CPU.
|
||||||
|
|
||||||
Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer.
|
Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer.
|
||||||
|
|
||||||
A CPU with AVX instructions is required for training and inference.
|
A CPU with AVX + AVX2 instructions is required for training and inference.
|
||||||
|
|
||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
@ -27,7 +27,6 @@ For object classification:
|
|||||||
### Classification Type
|
### Classification Type
|
||||||
|
|
||||||
- **Sub label**:
|
- **Sub label**:
|
||||||
|
|
||||||
- Applied to the object’s `sub_label` field.
|
- Applied to the object’s `sub_label` field.
|
||||||
- Ideal for a single, more specific identity or type.
|
- Ideal for a single, more specific identity or type.
|
||||||
- Example: `cat` → `Leo`, `Charlie`, `None`.
|
- Example: `cat` → `Leo`, `Charlie`, `None`.
|
||||||
@ -119,6 +118,7 @@ Enable debug logs for classification models by adding `frigate.data_processing.r
|
|||||||
logger:
|
logger:
|
||||||
default: info
|
default: info
|
||||||
logs:
|
logs:
|
||||||
|
# highlight-next-line
|
||||||
frigate.data_processing.real_time.custom_classification: debug
|
frigate.data_processing.real_time.custom_classification: debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,11 @@ State classification allows you to train a custom MobileNetV2 classification mod
|
|||||||
|
|
||||||
## Minimum System Requirements
|
## Minimum System Requirements
|
||||||
|
|
||||||
State classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate.
|
State classification models are lightweight and run very fast on CPU.
|
||||||
|
|
||||||
Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer.
|
Training the model does briefly use a high amount of system resources for about 1–3 minutes per training run. On lower-power devices, training may take longer.
|
||||||
|
|
||||||
A CPU with AVX instructions is required for training and inference.
|
A CPU with AVX + AVX2 instructions is required for training and inference.
|
||||||
|
|
||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
@ -85,6 +85,7 @@ Enable debug logs for classification models by adding `frigate.data_processing.r
|
|||||||
logger:
|
logger:
|
||||||
default: info
|
default: info
|
||||||
logs:
|
logs:
|
||||||
|
# highlight-next-line
|
||||||
frigate.data_processing.real_time.custom_classification: debug
|
frigate.data_processing.real_time.custom_classification: debug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,8 @@ All of these features run locally on your system.
|
|||||||
|
|
||||||
## Minimum System Requirements
|
## Minimum System Requirements
|
||||||
|
|
||||||
|
A CPU with AVX + AVX2 instructions is required to run Face Recognition.
|
||||||
|
|
||||||
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently.
|
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently.
|
||||||
|
|
||||||
The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
|
The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
|
||||||
@ -143,17 +145,14 @@ Start with the [Usage](#usage) section and re-read the [Model Requirements](#mod
|
|||||||
1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library.
|
1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library.
|
||||||
|
|
||||||
If you are using a Frigate+ or `face` detecting model:
|
If you are using a Frigate+ or `face` detecting model:
|
||||||
|
|
||||||
- Watch the debug view (Settings --> Debug) to ensure that `face` is being detected along with `person`.
|
- Watch the debug view (Settings --> Debug) to ensure that `face` is being detected along with `person`.
|
||||||
- You may need to adjust the `min_score` for the `face` object if faces are not being detected.
|
- You may need to adjust the `min_score` for the `face` object if faces are not being detected.
|
||||||
|
|
||||||
If you are **not** using a Frigate+ or `face` detecting model:
|
If you are **not** using a Frigate+ or `face` detecting model:
|
||||||
|
|
||||||
- Check your `detect` stream resolution and ensure it is sufficiently high enough to capture face details on `person` objects.
|
- Check your `detect` stream resolution and ensure it is sufficiently high enough to capture face details on `person` objects.
|
||||||
- You may need to lower your `detection_threshold` if faces are not being detected.
|
- You may need to lower your `detection_threshold` if faces are not being detected.
|
||||||
|
|
||||||
2. Any detected faces will then be _recognized_.
|
2. Any detected faces will then be _recognized_.
|
||||||
|
|
||||||
- Make sure you have trained at least one face per the recommendations above.
|
- Make sure you have trained at least one face per the recommendations above.
|
||||||
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
|
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
|
||||||
|
|
||||||
|
|||||||
@ -187,7 +187,7 @@ genai:
|
|||||||
|
|
||||||
To use a different Gemini-compatible API endpoint, set the `provider_options` with the `base_url` key to your provider's API URL. For example:
|
To use a different Gemini-compatible API endpoint, set the `provider_options` with the `base_url` key to your provider's API URL. For example:
|
||||||
|
|
||||||
```
|
```yaml {4,5}
|
||||||
genai:
|
genai:
|
||||||
provider: gemini
|
provider: gemini
|
||||||
...
|
...
|
||||||
@ -220,6 +220,29 @@ genai:
|
|||||||
model: gpt-4o
|
model: gpt-4o
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
To use a different OpenAI-compatible API endpoint, set the `OPENAI_BASE_URL` environment variable to your provider's API URL.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
For OpenAI-compatible servers (such as llama.cpp) that don't expose the configured context size in the API response, you can manually specify the context size in `provider_options`:
|
||||||
|
|
||||||
|
```yaml {5,6}
|
||||||
|
genai:
|
||||||
|
provider: openai
|
||||||
|
base_url: http://your-llama-server
|
||||||
|
model: your-model-name
|
||||||
|
provider_options:
|
||||||
|
context_size: 8192 # Specify the configured context size
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures Frigate uses the correct context window size when generating prompts.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
### Azure OpenAI
|
### Azure OpenAI
|
||||||
|
|
||||||
Microsoft offers several vision models through Azure OpenAI. A subscription is required.
|
Microsoft offers several vision models through Azure OpenAI. A subscription is required.
|
||||||
|
|||||||
@ -80,6 +80,7 @@ By default, review summaries use preview images (cached preview frames) which ha
|
|||||||
review:
|
review:
|
||||||
genai:
|
genai:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
# highlight-next-line
|
||||||
image_source: recordings # Options: "preview" (default) or "recordings"
|
image_source: recordings # Options: "preview" (default) or "recordings"
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ If recordings are not available for a given time period, the system will automat
|
|||||||
|
|
||||||
Along with the concern of suspicious activity or immediate threat, you may have concerns such as animals in your garden or a gate being left open. These concerns can be configured so that the review summaries will make note of them if the activity requires additional review. For example:
|
Along with the concern of suspicious activity or immediate threat, you may have concerns such as animals in your garden or a gate being left open. These concerns can be configured so that the review summaries will make note of them if the activity requires additional review. For example:
|
||||||
|
|
||||||
```yaml
|
```yaml {4,5}
|
||||||
review:
|
review:
|
||||||
genai:
|
genai:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -116,7 +117,7 @@ review:
|
|||||||
|
|
||||||
By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option:
|
By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option:
|
||||||
|
|
||||||
```yaml
|
```yaml {4}
|
||||||
review:
|
review:
|
||||||
genai:
|
genai:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import CommunityBadge from '@site/src/components/CommunityBadge';
|
|||||||
It is highly recommended to use an integrated or discrete GPU for hardware acceleration video decoding in Frigate.
|
It is highly recommended to use an integrated or discrete GPU for hardware acceleration video decoding in Frigate.
|
||||||
|
|
||||||
Some types of hardware acceleration are detected and used automatically, but you may need to update your configuration to enable hardware accelerated decoding in ffmpeg. To verify that hardware acceleration is working:
|
Some types of hardware acceleration are detected and used automatically, but you may need to update your configuration to enable hardware accelerated decoding in ffmpeg. To verify that hardware acceleration is working:
|
||||||
|
|
||||||
- Check the logs: A message will either say that hardware acceleration was automatically detected, or there will be a warning that no hardware acceleration was automatically detected
|
- Check the logs: A message will either say that hardware acceleration was automatically detected, or there will be a warning that no hardware acceleration was automatically detected
|
||||||
- If hardware acceleration is specified in the config, verification can be done by ensuring the logs are free from errors. There is no CPU fallback for hardware acceleration.
|
- If hardware acceleration is specified in the config, verification can be done by ensuring the logs are free from errors. There is no CPU fallback for hardware acceleration.
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video
|
|||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|
||||||
The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA Add-on users](advanced.md#environment_vars).
|
The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
|
||||||
|
|
||||||
See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is.
|
See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is.
|
||||||
|
|
||||||
@ -116,12 +117,13 @@ services:
|
|||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
image: ghcr.io/blakeblackshear/frigate:stable
|
image: ghcr.io/blakeblackshear/frigate:stable
|
||||||
|
# highlight-next-line
|
||||||
privileged: true
|
privileged: true
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Docker Run CLI - Privileged
|
##### Docker Run CLI - Privileged
|
||||||
|
|
||||||
```bash
|
```bash {4}
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name frigate \
|
--name frigate \
|
||||||
...
|
...
|
||||||
@ -135,7 +137,7 @@ Only recent versions of Docker support the `CAP_PERFMON` capability. You can tes
|
|||||||
|
|
||||||
##### Docker Compose - CAP_PERFMON
|
##### Docker Compose - CAP_PERFMON
|
||||||
|
|
||||||
```yaml
|
```yaml {5,6}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -146,7 +148,7 @@ services:
|
|||||||
|
|
||||||
##### Docker Run CLI - CAP_PERFMON
|
##### Docker Run CLI - CAP_PERFMON
|
||||||
|
|
||||||
```bash
|
```bash {4}
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name frigate \
|
--name frigate \
|
||||||
...
|
...
|
||||||
@ -188,7 +190,7 @@ Frigate can utilize modern AMD integrated GPUs and AMD GPUs to accelerate video
|
|||||||
|
|
||||||
### Configuring Radeon Driver
|
### Configuring Radeon Driver
|
||||||
|
|
||||||
You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA Add-on users](advanced.md#environment_vars).
|
You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
|
||||||
|
|
||||||
### Via VAAPI
|
### Via VAAPI
|
||||||
|
|
||||||
@ -213,7 +215,7 @@ Additional configuration is needed for the Docker container to be able to access
|
|||||||
|
|
||||||
#### Docker Compose - Nvidia GPU
|
#### Docker Compose - Nvidia GPU
|
||||||
|
|
||||||
```yaml
|
```yaml {5-12}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -230,7 +232,7 @@ services:
|
|||||||
|
|
||||||
#### Docker Run CLI - Nvidia GPU
|
#### Docker Run CLI - Nvidia GPU
|
||||||
|
|
||||||
```bash
|
```bash {4}
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name frigate \
|
--name frigate \
|
||||||
...
|
...
|
||||||
@ -292,7 +294,7 @@ These instructions were originally based on the [Jellyfin documentation](https:/
|
|||||||
## Raspberry Pi 3/4
|
## Raspberry Pi 3/4
|
||||||
|
|
||||||
Ensure you increase the allocated RAM for your GPU to at least 128 (`raspi-config` > Performance Options > GPU Memory).
|
Ensure you increase the allocated RAM for your GPU to at least 128 (`raspi-config` > Performance Options > GPU Memory).
|
||||||
If you are using the HA Add-on, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration.
|
If you are using the HA App, you may need to use the full access variant and turn off _Protection mode_ for hardware acceleration.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# if you want to decode a h264 stream
|
# if you want to decode a h264 stream
|
||||||
@ -309,7 +311,7 @@ ffmpeg:
|
|||||||
If running Frigate through Docker, you either need to run in privileged mode or
|
If running Frigate through Docker, you either need to run in privileged mode or
|
||||||
map the `/dev/video*` devices to Frigate. With Docker Compose add:
|
map the `/dev/video*` devices to Frigate. With Docker Compose add:
|
||||||
|
|
||||||
```yaml
|
```yaml {4-5}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -319,7 +321,7 @@ services:
|
|||||||
|
|
||||||
Or with `docker run`:
|
Or with `docker run`:
|
||||||
|
|
||||||
```bash
|
```bash {4}
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name frigate \
|
--name frigate \
|
||||||
...
|
...
|
||||||
@ -351,7 +353,7 @@ You will need to use the image with the nvidia container runtime:
|
|||||||
|
|
||||||
### Docker Run CLI - Jetson
|
### Docker Run CLI - Jetson
|
||||||
|
|
||||||
```bash
|
```bash {3}
|
||||||
docker run -d \
|
docker run -d \
|
||||||
...
|
...
|
||||||
--runtime nvidia
|
--runtime nvidia
|
||||||
@ -360,7 +362,7 @@ docker run -d \
|
|||||||
|
|
||||||
### Docker Compose - Jetson
|
### Docker Compose - Jetson
|
||||||
|
|
||||||
```yaml
|
```yaml {5}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -451,14 +453,14 @@ Restarting ffmpeg...
|
|||||||
|
|
||||||
you should try to uprade to FFmpeg 7. This can be done using this config option:
|
you should try to uprade to FFmpeg 7. This can be done using this config option:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
path: "7.0"
|
path: "7.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
You can set this option globally to use FFmpeg 7 for all cameras or on camera level to use it only for specific cameras. Do not confuse this option with:
|
You can set this option globally to use FFmpeg 7 for all cameras or on camera level to use it only for specific cameras. Do not confuse this option with:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
cameras:
|
cameras:
|
||||||
name:
|
name:
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
@ -480,7 +482,7 @@ Make sure to follow the [Synaptics specific installation instructions](/frigate/
|
|||||||
|
|
||||||
Add one of the following FFmpeg presets to your `config.yml` to enable hardware video processing:
|
Add one of the following FFmpeg presets to your `config.yml` to enable hardware video processing:
|
||||||
|
|
||||||
```yaml
|
```yaml {2}
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
hwaccel_args: -c:v h264_v4l2m2m
|
hwaccel_args: -c:v h264_v4l2m2m
|
||||||
input_args: preset-rtsp-restream
|
input_args: preset-rtsp-restream
|
||||||
|
|||||||
@ -3,7 +3,7 @@ id: index
|
|||||||
title: Frigate Configuration
|
title: Frigate Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
For Home Assistant Add-on installations, the config file should be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate Add-on you are running. See the list of directories [here](#accessing-add-on-config-dir).
|
For Home Assistant App installations, the config file should be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate App you are running. See the list of directories [here](#accessing-app-config-dir).
|
||||||
|
|
||||||
For all other installation types, the config file should be mapped to `/config/config.yml` inside the container.
|
For all other installation types, the config file should be mapped to `/config/config.yml` inside the container.
|
||||||
|
|
||||||
@ -25,11 +25,11 @@ cameras:
|
|||||||
- detect
|
- detect
|
||||||
```
|
```
|
||||||
|
|
||||||
## Accessing the Home Assistant Add-on configuration directory {#accessing-add-on-config-dir}
|
## Accessing the Home Assistant App configuration directory {#accessing-app-config-dir}
|
||||||
|
|
||||||
When running Frigate through the HA Add-on, the Frigate `/config` directory is mapped to `/addon_configs/<addon_directory>` in the host, where `<addon_directory>` is specific to the variant of the Frigate Add-on you are running.
|
When running Frigate through the HA App, the Frigate `/config` directory is mapped to `/addon_configs/<addon_directory>` in the host, where `<addon_directory>` is specific to the variant of the Frigate App you are running.
|
||||||
|
|
||||||
| Add-on Variant | Configuration directory |
|
| App Variant | Configuration directory |
|
||||||
| -------------------------- | ----------------------------------------- |
|
| -------------------------- | ----------------------------------------- |
|
||||||
| Frigate | `/addon_configs/ccab4aaf_frigate` |
|
| Frigate | `/addon_configs/ccab4aaf_frigate` |
|
||||||
| Frigate (Full Access) | `/addon_configs/ccab4aaf_frigate-fa` |
|
| Frigate (Full Access) | `/addon_configs/ccab4aaf_frigate-fa` |
|
||||||
@ -38,11 +38,11 @@ When running Frigate through the HA Add-on, the Frigate `/config` directory is m
|
|||||||
|
|
||||||
**Whenever you see `/config` in the documentation, it refers to this directory.**
|
**Whenever you see `/config` in the documentation, it refers to this directory.**
|
||||||
|
|
||||||
If for example you are running the standard Add-on variant and use the [VS Code Add-on](https://github.com/hassio-addons/addon-vscode) to browse your files, you can click _File_ > _Open folder..._ and navigate to `/addon_configs/ccab4aaf_frigate` to access the Frigate `/config` directory and edit the `config.yaml` file. You can also use the built-in file editor in the Frigate UI to edit the configuration file.
|
If for example you are running the standard App variant and use the [VS Code App](https://github.com/hassio-addons/addon-vscode) to browse your files, you can click _File_ > _Open folder..._ and navigate to `/addon_configs/ccab4aaf_frigate` to access the Frigate `/config` directory and edit the `config.yaml` file. You can also use the built-in file editor in the Frigate UI to edit the configuration file.
|
||||||
|
|
||||||
## VS Code Configuration Schema
|
## VS Code Configuration Schema
|
||||||
|
|
||||||
VS Code supports JSON schemas for automatically validating configuration files. You can enable this feature by adding `# yaml-language-server: $schema=http://frigate_host:5000/api/config/schema.json` to the beginning of the configuration file. Replace `frigate_host` with the IP address or hostname of your Frigate server. If you're using both VS Code and Frigate as an Add-on, you should use `ccab4aaf-frigate` instead. Make sure to expose the internal unauthenticated port `5000` when accessing the config from VS Code on another machine.
|
VS Code supports JSON schemas for automatically validating configuration files. You can enable this feature by adding `# yaml-language-server: $schema=http://frigate_host:5000/api/config/schema.json` to the beginning of the configuration file. Replace `frigate_host` with the IP address or hostname of your Frigate server. If you're using both VS Code and Frigate as an App, you should use `ccab4aaf-frigate` instead. Make sure to expose the internal unauthenticated port `5000` when accessing the config from VS Code on another machine.
|
||||||
|
|
||||||
## Environment Variable Substitution
|
## Environment Variable Substitution
|
||||||
|
|
||||||
@ -50,6 +50,7 @@ Frigate supports the use of environment variables starting with `FRIGATE_` **onl
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
mqtt:
|
mqtt:
|
||||||
|
host: "{FRIGATE_MQTT_HOST}"
|
||||||
user: "{FRIGATE_MQTT_USER}"
|
user: "{FRIGATE_MQTT_USER}"
|
||||||
password: "{FRIGATE_MQTT_PASSWORD}"
|
password: "{FRIGATE_MQTT_PASSWORD}"
|
||||||
```
|
```
|
||||||
@ -60,7 +61,7 @@ mqtt:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
onvif:
|
onvif:
|
||||||
host: 10.0.10.10
|
host: "192.168.1.12"
|
||||||
port: 8000
|
port: 8000
|
||||||
user: "{FRIGATE_RTSP_USER}"
|
user: "{FRIGATE_RTSP_USER}"
|
||||||
password: "{FRIGATE_RTSP_PASSWORD}"
|
password: "{FRIGATE_RTSP_PASSWORD}"
|
||||||
@ -82,10 +83,10 @@ genai:
|
|||||||
|
|
||||||
Here are some common starter configuration examples. Refer to the [reference config](./reference.md) for detailed information about all the config values.
|
Here are some common starter configuration examples. Refer to the [reference config](./reference.md) for detailed information about all the config values.
|
||||||
|
|
||||||
### Raspberry Pi Home Assistant Add-on with USB Coral
|
### Raspberry Pi Home Assistant App with USB Coral
|
||||||
|
|
||||||
- Single camera with 720p, 5fps stream for detect
|
- Single camera with 720p, 5fps stream for detect
|
||||||
- MQTT connected to the Home Assistant Mosquitto Add-on
|
- MQTT connected to the Home Assistant Mosquitto App
|
||||||
- Hardware acceleration for decoding video
|
- Hardware acceleration for decoding video
|
||||||
- USB Coral detector
|
- USB Coral detector
|
||||||
- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not
|
- Save all video with any detectable motion for 7 days regardless of whether any objects were detected or not
|
||||||
|
|||||||
@ -30,7 +30,7 @@ In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle`
|
|||||||
|
|
||||||
## Minimum System Requirements
|
## Minimum System Requirements
|
||||||
|
|
||||||
License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM is required.
|
License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM and a CPU with AVX + AVX2 instructions is required.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ lpr:
|
|||||||
|
|
||||||
Like other enrichments in Frigate, LPR **must be enabled globally** to use the feature. You should disable it for specific cameras at the camera level if you don't want to run LPR on cars on those cameras:
|
Like other enrichments in Frigate, LPR **must be enabled globally** to use the feature. You should disable it for specific cameras at the camera level if you don't want to run LPR on cars on those cameras:
|
||||||
|
|
||||||
```yaml
|
```yaml {4,5}
|
||||||
cameras:
|
cameras:
|
||||||
garage:
|
garage:
|
||||||
...
|
...
|
||||||
@ -375,7 +375,6 @@ Use `match_distance` to allow small character mismatches. Alternatively, define
|
|||||||
Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps.
|
Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps.
|
||||||
|
|
||||||
1. Start with a simplified LPR config.
|
1. Start with a simplified LPR config.
|
||||||
|
|
||||||
- Remove or comment out everything in your LPR config, including `min_area`, `min_plate_length`, `format`, `known_plates`, or `enhancement` values so that the only values left are `enabled` and `debug_save_plates`. This will run LPR with Frigate's default values.
|
- Remove or comment out everything in your LPR config, including `min_area`, `min_plate_length`, `format`, `known_plates`, or `enhancement` values so that the only values left are `enabled` and `debug_save_plates`. This will run LPR with Frigate's default values.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -386,31 +385,28 @@ Start with ["Why isn't my license plate being detected and recognized?"](#why-is
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Enable debug logs to see exactly what Frigate is doing.
|
2. Enable debug logs to see exactly what Frigate is doing.
|
||||||
|
|
||||||
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary. Restart Frigate after this change.
|
- Enable debug logs for LPR by adding `frigate.data_processing.common.license_plate: debug` to your `logger` configuration. These logs are _very_ verbose, so only keep this enabled when necessary. Restart Frigate after this change.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
logger:
|
logger:
|
||||||
default: info
|
default: info
|
||||||
logs:
|
logs:
|
||||||
|
# highlight-next-line
|
||||||
frigate.data_processing.common.license_plate: debug
|
frigate.data_processing.common.license_plate: debug
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Ensure your plates are being _detected_.
|
3. Ensure your plates are being _detected_.
|
||||||
|
|
||||||
If you are using a Frigate+ or `license_plate` detecting model:
|
If you are using a Frigate+ or `license_plate` detecting model:
|
||||||
|
|
||||||
- Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected.
|
- Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected.
|
||||||
- View MQTT messages for `frigate/events` to verify detected plates.
|
- View MQTT messages for `frigate/events` to verify detected plates.
|
||||||
- You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected.
|
- You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected.
|
||||||
|
|
||||||
If you are **not** using a Frigate+ or `license_plate` detecting model:
|
If you are **not** using a Frigate+ or `license_plate` detecting model:
|
||||||
|
|
||||||
- Watch the debug logs for messages from the YOLOv9 plate detector.
|
- Watch the debug logs for messages from the YOLOv9 plate detector.
|
||||||
- You may need to adjust your `detection_threshold` if your plates are not being detected.
|
- You may need to adjust your `detection_threshold` if your plates are not being detected.
|
||||||
|
|
||||||
4. Ensure the characters on detected plates are being _recognized_.
|
4. Ensure the characters on detected plates are being _recognized_.
|
||||||
|
|
||||||
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
|
- Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear.
|
||||||
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
|
- Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working.
|
||||||
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
|
- Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration).
|
||||||
|
|||||||
@ -15,7 +15,7 @@ The jsmpeg live view will use more browser and client GPU resources. Using go2rt
|
|||||||
| ------ | ------------------------------------- | ---------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------ | ------------------------------------- | ---------- | ---------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| jsmpeg | same as `detect -> fps`, capped at 10 | 720p | no | no | Resolution is configurable, but go2rtc is recommended if you want higher resolutions and better frame rates. jsmpeg is Frigate's default without go2rtc configured. |
|
| jsmpeg | same as `detect -> fps`, capped at 10 | 720p | no | no | Resolution is configurable, but go2rtc is recommended if you want higher resolutions and better frame rates. jsmpeg is Frigate's default without go2rtc configured. |
|
||||||
| mse | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only. This is Frigate's default when go2rtc is configured. |
|
| mse | native | native | yes (depends on audio codec) | yes | iPhone requires iOS 17.1+, Firefox is h.264 only. This is Frigate's default when go2rtc is configured. |
|
||||||
| webrtc | native | native | yes (depends on audio codec) | yes | Requires extra configuration. Frigate attempts to use WebRTC when MSE fails or when using a camera's two-way talk feature. |
|
| webrtc | native | native | yes (depends on audio codec) | yes | Requires extra configuration. Frigate attempts to use WebRTC when MSE fails or when using a camera's two-way talk feature. |
|
||||||
|
|
||||||
### Camera Settings Recommendations
|
### Camera Settings Recommendations
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ Configure the `streams` option with a "friendly name" for your stream followed b
|
|||||||
|
|
||||||
Using Frigate's internal version of go2rtc is required to use this feature. You cannot specify paths in the `streams` configuration, only go2rtc stream names.
|
Using Frigate's internal version of go2rtc is required to use this feature. You cannot specify paths in the `streams` configuration, only go2rtc stream names.
|
||||||
|
|
||||||
```yaml
|
```yaml {3,6,8,25-29}
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
test_cam:
|
test_cam:
|
||||||
@ -114,9 +114,9 @@ cameras:
|
|||||||
WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration:
|
WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration:
|
||||||
|
|
||||||
- For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP.
|
- For external access, over the internet, setup your router to forward port `8555` to port `8555` on the Frigate device, for both TCP and UDP.
|
||||||
- For internal/local access, unless you are running through the HA Add-on, you will also need to set the WebRTC candidates list in the go2rtc config. For example, if `192.168.1.10` is the local IP of the device running Frigate:
|
- For internal/local access, unless you are running through the HA App, you will also need to set the WebRTC candidates list in the go2rtc config. For example, if `192.168.1.10` is the local IP of the device running Frigate:
|
||||||
|
|
||||||
```yaml title="config.yml"
|
```yaml title="config.yml" {4-7}
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
test_cam: ...
|
test_cam: ...
|
||||||
@ -128,13 +128,13 @@ WebRTC works by creating a TCP or UDP connection on port `8555`. However, it req
|
|||||||
|
|
||||||
- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block.
|
- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block.
|
||||||
|
|
||||||
- Note that some browsers may not support H.265 (HEVC). You can check your browser's current version for H.265 compatibility [here](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness).
|
- Note that some browsers may not support H.265 (HEVC). You can check your browser's current version for H.265 compatibility [here](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness).
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
This extra configuration may not be required if Frigate has been installed as a Home Assistant Add-on, as Frigate uses the Supervisor's API to generate a WebRTC candidate.
|
This extra configuration may not be required if Frigate has been installed as a Home Assistant App, as Frigate uses the Supervisor's API to generate a WebRTC candidate.
|
||||||
|
|
||||||
However, it is recommended if issues occur to define the candidates manually. You should do this if the Frigate Add-on fails to generate a valid candidate. If an error occurs you will see some warnings like the below in the Add-on logs page during the initialization:
|
However, it is recommended if issues occur to define the candidates manually. You should do this if the Frigate App fails to generate a valid candidate. If an error occurs you will see some warnings like the below in the App logs page during the initialization:
|
||||||
|
|
||||||
```log
|
```log
|
||||||
[WARN] Failed to get IP address from supervisor
|
[WARN] Failed to get IP address from supervisor
|
||||||
@ -154,7 +154,7 @@ If not running in host mode, port 8555 will need to be mapped for the container:
|
|||||||
|
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
```yaml
|
```yaml {4-6}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -222,34 +222,28 @@ Note that disabling a camera through the config file (`enabled: False`) removes
|
|||||||
When your browser runs into problems playing back your camera streams, it will log short error messages to the browser console. They indicate playback, codec, or network issues on the client/browser side, not something server side with Frigate itself. Below are the common messages you may see and simple actions you can take to try to resolve them.
|
When your browser runs into problems playing back your camera streams, it will log short error messages to the browser console. They indicate playback, codec, or network issues on the client/browser side, not something server side with Frigate itself. Below are the common messages you may see and simple actions you can take to try to resolve them.
|
||||||
|
|
||||||
- **startup**
|
- **startup**
|
||||||
|
|
||||||
- What it means: The player failed to initialize or connect to the live stream (network or startup error).
|
- What it means: The player failed to initialize or connect to the live stream (network or startup error).
|
||||||
- What to try: Reload the Live view or click _Reset_. Verify `go2rtc` is running and the camera stream is reachable. Try switching to a different stream from the Live UI dropdown (if available) or use a different browser.
|
- What to try: Reload the Live view or click _Reset_. Verify `go2rtc` is running and the camera stream is reachable. Try switching to a different stream from the Live UI dropdown (if available) or use a different browser.
|
||||||
|
|
||||||
- Possible console messages from the player code:
|
- Possible console messages from the player code:
|
||||||
|
|
||||||
- `Error opening MediaSource.`
|
- `Error opening MediaSource.`
|
||||||
- `Browser reported a network error.`
|
- `Browser reported a network error.`
|
||||||
- `Max error count ${errorCount} exceeded.` (the numeric value will vary)
|
- `Max error count ${errorCount} exceeded.` (the numeric value will vary)
|
||||||
|
|
||||||
- **mse-decode**
|
- **mse-decode**
|
||||||
|
|
||||||
- What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames.
|
- What it means: The browser reported a decoding error while trying to play the stream, which usually is a result of a codec incompatibility or corrupted frames.
|
||||||
- What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer.
|
- What to try: Check the browser console for the supported and negotiated codecs. Ensure your camera/restream is using H.264 video and AAC audio (these are the most compatible). If your camera uses a non-standard audio codec, configure `go2rtc` to transcode the stream to AAC. Try another browser (some browsers have stricter MSE/codec support) and, for iPhone, ensure you're on iOS 17.1 or newer.
|
||||||
|
|
||||||
- Possible console messages from the player code:
|
- Possible console messages from the player code:
|
||||||
|
|
||||||
- `Safari cannot open MediaSource.`
|
- `Safari cannot open MediaSource.`
|
||||||
- `Safari reported InvalidStateError.`
|
- `Safari reported InvalidStateError.`
|
||||||
- `Safari reported decoding errors.`
|
- `Safari reported decoding errors.`
|
||||||
|
|
||||||
- **stalled**
|
- **stalled**
|
||||||
|
|
||||||
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
|
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
|
||||||
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval — shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
|
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval — shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings.
|
||||||
|
|
||||||
- Possible console messages from the player code:
|
- Possible console messages from the player code:
|
||||||
|
|
||||||
- `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.`
|
- `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.`
|
||||||
- `Media playback has stalled after <n> seconds due to insufficient buffering or a network interruption.` (the seconds value will vary)
|
- `Media playback has stalled after <n> seconds due to insufficient buffering or a network interruption.` (the seconds value will vary)
|
||||||
|
|
||||||
@ -270,21 +264,18 @@ When your browser runs into problems playing back your camera streams, it will l
|
|||||||
If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again.
|
If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again.
|
||||||
|
|
||||||
Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include:
|
Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include:
|
||||||
|
|
||||||
- Network issues (e.g., MSE or WebRTC network connection problems).
|
- Network issues (e.g., MSE or WebRTC network connection problems).
|
||||||
- Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers).
|
- Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers).
|
||||||
- Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg.
|
- Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg.
|
||||||
- Browser compatibility problems (e.g., iOS Safari limitations with MSE).
|
- Browser compatibility problems (e.g., iOS Safari limitations with MSE).
|
||||||
|
|
||||||
To view browser console logs:
|
To view browser console logs:
|
||||||
|
|
||||||
1. Open the Frigate Live View in your browser.
|
1. Open the Frigate Live View in your browser.
|
||||||
2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab).
|
2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab).
|
||||||
3. Reproduce the error (e.g., load a problematic stream or simulate network issues).
|
3. Reproduce the error (e.g., load a problematic stream or simulate network issues).
|
||||||
4. Look for messages prefixed with the camera name.
|
4. Look for messages prefixed with the camera name.
|
||||||
|
|
||||||
These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors:
|
These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors:
|
||||||
|
|
||||||
- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera_settings_recommendations)).
|
- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera_settings_recommendations)).
|
||||||
- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS).
|
- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS).
|
||||||
- Test with a different stream via the UI dropdown (if `live -> streams` is configured).
|
- Test with a different stream via the UI dropdown (if `live -> streams` is configured).
|
||||||
@ -324,9 +315,7 @@ When your browser runs into problems playing back your camera streams, it will l
|
|||||||
To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches.
|
To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches.
|
||||||
|
|
||||||
Example: Resolutions from two streams
|
Example: Resolutions from two streams
|
||||||
|
|
||||||
- Mismatched (may cause aspect ratio switching on the dashboard):
|
- Mismatched (may cause aspect ratio switching on the dashboard):
|
||||||
|
|
||||||
- Live/go2rtc stream: 1920x1080 (16:9)
|
- Live/go2rtc stream: 1920x1080 (16:9)
|
||||||
- Detect stream: 640x352 (~1.82:1, not 16:9)
|
- Detect stream: 640x352 (~1.82:1, not 16:9)
|
||||||
|
|
||||||
|
|||||||
@ -166,7 +166,7 @@ YOLOv9 models that are compiled for TensorFlow Lite and properly quantized are s
|
|||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
**Frigate+ Users:** Follow the [instructions](../integrations/plus#use-models) to set a model ID in your config file.
|
**Frigate+ Users:** Follow the [instructions](/integrations/plus#use-models) to set a model ID in your config file.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -577,7 +577,7 @@ $ docker run --device=/dev/kfd --device=/dev/dri \
|
|||||||
|
|
||||||
When using Docker Compose:
|
When using Docker Compose:
|
||||||
|
|
||||||
```yaml
|
```yaml {4-6}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -608,7 +608,7 @@ $ docker run -e HSA_OVERRIDE_GFX_VERSION=10.0.0 \
|
|||||||
|
|
||||||
When using Docker Compose:
|
When using Docker Compose:
|
||||||
|
|
||||||
```yaml
|
```yaml {4-5}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
|
|||||||
@ -130,7 +130,7 @@ When exporting a time-lapse the default speed-up is 25x with 30 FPS. This means
|
|||||||
|
|
||||||
To configure the speed-up factor, the frame rate and further custom settings, the configuration parameter `timelapse_args` can be used. The below configuration example would change the time-lapse speed to 60x (for fitting 1 hour of recording into 1 minute of time-lapse) with 25 FPS:
|
To configure the speed-up factor, the frame rate and further custom settings, the configuration parameter `timelapse_args` can be used. The below configuration example would change the time-lapse speed to 60x (for fitting 1 hour of recording into 1 minute of time-lapse) with 25 FPS:
|
||||||
|
|
||||||
```yaml
|
```yaml {3-4}
|
||||||
record:
|
record:
|
||||||
enabled: True
|
enabled: True
|
||||||
export:
|
export:
|
||||||
|
|||||||
@ -16,6 +16,8 @@ mqtt:
|
|||||||
# Optional: Enable mqtt server (default: shown below)
|
# Optional: Enable mqtt server (default: shown below)
|
||||||
enabled: True
|
enabled: True
|
||||||
# Required: host name
|
# Required: host name
|
||||||
|
# NOTE: MQTT host can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'.
|
||||||
|
# e.g. host: '{FRIGATE_MQTT_HOST}'
|
||||||
host: mqtt.server.com
|
host: mqtt.server.com
|
||||||
# Optional: port (default: shown below)
|
# Optional: port (default: shown below)
|
||||||
port: 1883
|
port: 1883
|
||||||
@ -949,6 +951,8 @@ cameras:
|
|||||||
onvif:
|
onvif:
|
||||||
# Required: host of the camera being connected to.
|
# Required: host of the camera being connected to.
|
||||||
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
|
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
|
||||||
|
# NOTE: ONVIF user, and password can be specified with environment variables or docker secrets
|
||||||
|
# that must begin with 'FRIGATE_'. e.g. host: '{FRIGATE_ONVIF_USERNAME}'
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
# Optional: ONVIF port for device (default: shown below).
|
# Optional: ONVIF port for device (default: shown below).
|
||||||
port: 8000
|
port: 8000
|
||||||
|
|||||||
@ -34,7 +34,7 @@ To improve connection speed when using Birdseye via restream you can enable a sm
|
|||||||
|
|
||||||
The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
|
The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
|
||||||
|
|
||||||
```yaml
|
```yaml {2-4}
|
||||||
go2rtc:
|
go2rtc:
|
||||||
rtsp:
|
rtsp:
|
||||||
username: "admin"
|
username: "admin"
|
||||||
@ -147,6 +147,7 @@ For example:
|
|||||||
```yaml
|
```yaml
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
|
# highlight-error-line
|
||||||
my_camera: rtsp://username:$@foo%@192.168.1.100
|
my_camera: rtsp://username:$@foo%@192.168.1.100
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ becomes
|
|||||||
```yaml
|
```yaml
|
||||||
go2rtc:
|
go2rtc:
|
||||||
streams:
|
streams:
|
||||||
|
# highlight-next-line
|
||||||
my_camera: rtsp://username:$%40foo%25@192.168.1.100
|
my_camera: rtsp://username:$%40foo%25@192.168.1.100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -71,7 +71,7 @@ To exclude a specific camera from alerts or detections, simply provide an empty
|
|||||||
|
|
||||||
For example, to exclude objects on the camera _gatecamera_ from any detections, include this in your config:
|
For example, to exclude objects on the camera _gatecamera_ from any detections, include this in your config:
|
||||||
|
|
||||||
```yaml
|
```yaml {3-5}
|
||||||
cameras:
|
cameras:
|
||||||
gatecamera:
|
gatecamera:
|
||||||
review:
|
review:
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Semantic Search is accessed via the _Explore_ view in the Frigate UI.
|
|||||||
|
|
||||||
Semantic Search works by running a large AI model locally on your system. Small or underpowered systems like a Raspberry Pi will not run Semantic Search reliably or at all.
|
Semantic Search works by running a large AI model locally on your system. Small or underpowered systems like a Raspberry Pi will not run Semantic Search reliably or at all.
|
||||||
|
|
||||||
A minimum of 8GB of RAM is required to use Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems.
|
A minimum of 8GB of RAM is required to use Semantic Search. A CPU with AVX + AVX2 instructions is required to run Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems.
|
||||||
|
|
||||||
For best performance, 16GB or more of RAM and a dedicated GPU are recommended.
|
For best performance, 16GB or more of RAM and a dedicated GPU are recommended.
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ tls:
|
|||||||
|
|
||||||
TLS certificates can be mounted at `/etc/letsencrypt/live/frigate` using a bind mount or docker volume.
|
TLS certificates can be mounted at `/etc/letsencrypt/live/frigate` using a bind mount or docker volume.
|
||||||
|
|
||||||
```yaml
|
```yaml {3-4}
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
volumes:
|
volumes:
|
||||||
@ -32,7 +32,7 @@ Within the folder, the private key is expected to be named `privkey.pem` and the
|
|||||||
|
|
||||||
Note that certbot uses symlinks, and those can't be followed by the container unless it has access to the targets as well, so if using certbot you'll also have to mount the `archive` folder for your domain, e.g.:
|
Note that certbot uses symlinks, and those can't be followed by the container unless it has access to the targets as well, so if using certbot you'll also have to mount the `archive` folder for your domain, e.g.:
|
||||||
|
|
||||||
```yaml
|
```yaml {3-5}
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
volumes:
|
volumes:
|
||||||
@ -46,7 +46,7 @@ Frigate automatically compares the fingerprint of the certificate at `/etc/letse
|
|||||||
|
|
||||||
If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8971 to 443.
|
If you issue Frigate valid certificates you will likely want to configure it to run on port 443 so you can access it without a port number like `https://your-frigate-domain.com` by mapping 8971 to 443.
|
||||||
|
|
||||||
```yaml
|
```yaml {3-4}
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -22,7 +22,7 @@ To create a zone, follow [the steps for a "Motion mask"](masks.md), but use the
|
|||||||
|
|
||||||
Often you will only want alerts to be created when an object enters areas of interest. This is done using zones along with setting required_zones. Let's say you only want to have an alert created when an object enters your entire_yard zone, the config would be:
|
Often you will only want alerts to be created when an object enters areas of interest. This is done using zones along with setting required_zones. Let's say you only want to have an alert created when an object enters your entire_yard zone, the config would be:
|
||||||
|
|
||||||
```yaml
|
```yaml {6,8}
|
||||||
cameras:
|
cameras:
|
||||||
name_of_your_camera:
|
name_of_your_camera:
|
||||||
review:
|
review:
|
||||||
@ -108,6 +108,7 @@ cameras:
|
|||||||
name_of_your_camera:
|
name_of_your_camera:
|
||||||
zones:
|
zones:
|
||||||
sidewalk:
|
sidewalk:
|
||||||
|
# highlight-next-line
|
||||||
loitering_time: 4 # unit is in seconds
|
loitering_time: 4 # unit is in seconds
|
||||||
objects:
|
objects:
|
||||||
- person
|
- person
|
||||||
@ -122,6 +123,7 @@ cameras:
|
|||||||
name_of_your_camera:
|
name_of_your_camera:
|
||||||
zones:
|
zones:
|
||||||
front_yard:
|
front_yard:
|
||||||
|
# highlight-next-line
|
||||||
inertia: 3
|
inertia: 3
|
||||||
objects:
|
objects:
|
||||||
- person
|
- person
|
||||||
@ -134,6 +136,7 @@ cameras:
|
|||||||
name_of_your_camera:
|
name_of_your_camera:
|
||||||
zones:
|
zones:
|
||||||
driveway_entrance:
|
driveway_entrance:
|
||||||
|
# highlight-next-line
|
||||||
inertia: 1
|
inertia: 1
|
||||||
objects:
|
objects:
|
||||||
- car
|
- car
|
||||||
@ -196,5 +199,6 @@ cameras:
|
|||||||
coordinates: ...
|
coordinates: ...
|
||||||
distances: ...
|
distances: ...
|
||||||
inertia: 1
|
inertia: 1
|
||||||
|
# highlight-next-line
|
||||||
speed_threshold: 20 # unit is in kph or mph, depending on how unit_system is set (see above)
|
speed_threshold: 20 # unit is in kph or mph, depending on how unit_system is set (see above)
|
||||||
```
|
```
|
||||||
|
|||||||
@ -17,15 +17,15 @@ From here, follow the guides for:
|
|||||||
- [Web Interface](#web-interface)
|
- [Web Interface](#web-interface)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
|
|
||||||
### Frigate Home Assistant Add-on
|
### Frigate Home Assistant App
|
||||||
|
|
||||||
This repository holds the Home Assistant Add-on, for use with Home Assistant OS and compatible installations. It is the piece that allows you to run Frigate from your Home Assistant Supervisor tab.
|
This repository holds the Home Assistant App, for use with Home Assistant OS and compatible installations. It is the piece that allows you to run Frigate from your Home Assistant Supervisor tab.
|
||||||
|
|
||||||
Fork [blakeblackshear/frigate-hass-addons](https://github.com/blakeblackshear/frigate-hass-addons) to your own Github profile, then clone the forked repo to your local machine.
|
Fork [blakeblackshear/frigate-hass-addons](https://github.com/blakeblackshear/frigate-hass-addons) to your own Github profile, then clone the forked repo to your local machine.
|
||||||
|
|
||||||
### Frigate Home Assistant Integration
|
### Frigate Home Assistant Integration
|
||||||
|
|
||||||
This repository holds the custom integration that allows your Home Assistant installation to automatically create entities for your Frigate instance, whether you are running Frigate as a standalone Docker container or as a [Home Assistant Add-on](#frigate-home-assistant-add-on).
|
This repository holds the custom integration that allows your Home Assistant installation to automatically create entities for your Frigate instance, whether you are running Frigate as a standalone Docker container or as a [Home Assistant App](#frigate-home-assistant-app).
|
||||||
|
|
||||||
Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshear/frigate-hass-integration) to your own GitHub profile, then clone the forked repo to your local machine.
|
Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshear/frigate-hass-integration) to your own GitHub profile, then clone the forked repo to your local machine.
|
||||||
|
|
||||||
@ -89,6 +89,14 @@ After closing VS Code, you may still have containers running. To close everythin
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
#### Unit Tests
|
||||||
|
|
||||||
|
GitHub will execute unit tests on new PRs. You must ensure that all tests pass.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python3 -u -m unittest
|
||||||
|
```
|
||||||
|
|
||||||
#### FFMPEG Hardware Acceleration
|
#### FFMPEG Hardware Acceleration
|
||||||
|
|
||||||
The following commands are used inside the container to ensure hardware acceleration is working properly.
|
The following commands are used inside the container to ensure hardware acceleration is working properly.
|
||||||
@ -125,6 +133,28 @@ ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format
|
|||||||
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Submitting a pull request
|
||||||
|
|
||||||
|
Code must be formatted, linted and type-tested. GitHub will run these checks on pull requests, so it is advised to run them yourself prior to opening.
|
||||||
|
|
||||||
|
**Formatting**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ruff format frigate migrations docker *.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linting**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
ruff check frigate migrations docker *.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**MyPy Static Typing**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
python3 -u -m mypy --config-file frigate/mypy.ini frigate
|
||||||
|
```
|
||||||
|
|
||||||
## Web Interface
|
## Web Interface
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|||||||
@ -26,7 +26,7 @@ I may earn a small commission for my endorsement, recommendation, testimonial, o
|
|||||||
|
|
||||||
## Server
|
## Server
|
||||||
|
|
||||||
My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators.
|
My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU (with AVX + AVX2 instructions) and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators.
|
||||||
|
|
||||||
Note that many of these mini PCs come with Windows pre-installed, and you will need to install Linux according to the [getting started guide](../guides/getting_started.md).
|
Note that many of these mini PCs come with Windows pre-installed, and you will need to install Linux according to the [getting started guide](../guides/getting_started.md).
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@ id: installation
|
|||||||
title: Installation
|
title: Installation
|
||||||
---
|
---
|
||||||
|
|
||||||
Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant Add-on](https://www.home-assistant.io/addons/). Note that the Home Assistant Add-on is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant Add-on.
|
Frigate is a Docker container that can be run on any Docker host including as a [Home Assistant App](https://www.home-assistant.io/apps/). Note that the Home Assistant App is **not** the same thing as the integration. The [integration](/integrations/home-assistant) is required to integrate Frigate into Home Assistant, whether you are running Frigate as a standalone Docker container or as a Home Assistant App.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
If you already have Frigate installed as a Home Assistant Add-on, check out the [getting started guide](../guides/getting_started#configuring-frigate) to configure Frigate.
|
If you already have Frigate installed as a Home Assistant App, check out the [getting started guide](../guides/getting_started#configuring-frigate) to configure Frigate.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ $ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 20 + 270480) / 1048576
|
|||||||
253MB
|
253MB
|
||||||
```
|
```
|
||||||
|
|
||||||
The shm size cannot be set per container for Home Assistant add-ons. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration.
|
The shm size cannot be set per container for Home Assistant Apps. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration.
|
||||||
|
|
||||||
## Extra Steps for Specific Hardware
|
## Extra Steps for Specific Hardware
|
||||||
|
|
||||||
@ -546,7 +546,7 @@ The community supported docker image tags for the current stable version are:
|
|||||||
- `stable-tensorrt-jp6` - Frigate build optimized for Nvidia Jetson devices running Jetpack 6
|
- `stable-tensorrt-jp6` - Frigate build optimized for Nvidia Jetson devices running Jetpack 6
|
||||||
- `stable-rk` - Frigate build for SBCs with Rockchip SoC
|
- `stable-rk` - Frigate build for SBCs with Rockchip SoC
|
||||||
|
|
||||||
## Home Assistant Add-on
|
## Home Assistant App
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
@ -557,7 +557,7 @@ There are important limitations in HA OS to be aware of:
|
|||||||
- Separate local storage for media is not yet supported by Home Assistant
|
- Separate local storage for media is not yet supported by Home Assistant
|
||||||
- AMD GPUs are not supported because HA OS does not include the mesa driver.
|
- AMD GPUs are not supported because HA OS does not include the mesa driver.
|
||||||
- Intel NPUs are not supported because HA OS does not include the NPU firmware.
|
- Intel NPUs are not supported because HA OS does not include the NPU firmware.
|
||||||
- Nvidia GPUs are not supported because addons do not support the Nvidia runtime.
|
- Nvidia GPUs are not supported because HA Apps do not support the Nvidia runtime.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -567,27 +567,27 @@ See [the network storage guide](/guides/ha_network_storage.md) for instructions
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Home Assistant OS users can install via the Add-on repository.
|
Home Assistant OS users can install via the App repository.
|
||||||
|
|
||||||
1. In Home Assistant, navigate to _Settings_ > _Add-ons_ > _Add-on Store_ > _Repositories_
|
1. In Home Assistant, navigate to _Settings_ > _Apps_ > _App Store_ > _Repositories_
|
||||||
2. Add `https://github.com/blakeblackshear/frigate-hass-addons`
|
2. Add `https://github.com/blakeblackshear/frigate-hass-addons`
|
||||||
3. Install the desired variant of the Frigate Add-on (see below)
|
3. Install the desired variant of the Frigate App (see below)
|
||||||
4. Setup your network configuration in the `Configuration` tab
|
4. Setup your network configuration in the `Configuration` tab
|
||||||
5. Start the Add-on
|
5. Start the App
|
||||||
6. Use the _Open Web UI_ button to access the Frigate UI, then click in the _cog icon_ > _Configuration editor_ and configure Frigate to your liking
|
6. Use the _Open Web UI_ button to access the Frigate UI, then click in the _cog icon_ > _Configuration editor_ and configure Frigate to your liking
|
||||||
|
|
||||||
There are several variants of the Add-on available:
|
There are several variants of the App available:
|
||||||
|
|
||||||
| Add-on Variant | Description |
|
| App Variant | Description |
|
||||||
| -------------------------- | ---------------------------------------------------------- |
|
| -------------------------- | ---------------------------------------------------------- |
|
||||||
| Frigate | Current release with protection mode on |
|
| Frigate | Current release with protection mode on |
|
||||||
| Frigate (Full Access) | Current release with the option to disable protection mode |
|
| Frigate (Full Access) | Current release with the option to disable protection mode |
|
||||||
| Frigate Beta | Beta release with protection mode on |
|
| Frigate Beta | Beta release with protection mode on |
|
||||||
| Frigate Beta (Full Access) | Beta release with the option to disable protection mode |
|
| Frigate Beta (Full Access) | Beta release with the option to disable protection mode |
|
||||||
|
|
||||||
If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the Add-on. This is because the Frigate Add-on runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system.
|
If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the App. This is because the Frigate App runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system.
|
||||||
|
|
||||||
You can also edit the Frigate configuration file through the [VS Code Add-on](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate Add-on you are running. See the list of directories [here](../configuration/index.md#accessing-add-on-config-dir).
|
You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/index.md#accessing-app-config-dir).
|
||||||
|
|
||||||
## Kubernetes
|
## Kubernetes
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,14 @@ For commercial installations it is important to verify the number of supported c
|
|||||||
|
|
||||||
There are many different hardware options for object detection depending on priorities and available hardware. See [the recommended hardware page](./hardware.md#detectors) for more specifics on what hardware is recommended for object detection.
|
There are many different hardware options for object detection depending on priorities and available hardware. See [the recommended hardware page](./hardware.md#detectors) for more specifics on what hardware is recommended for object detection.
|
||||||
|
|
||||||
|
### CPU
|
||||||
|
|
||||||
|
Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011) support AVX and AVX2, but it is generally absent in low-power or budget-oriented processors, particularly older Intel Pentium, Celeron, and Atom-based chips. Specifically, Intel Celeron and Pentium models prior to the 2020 Tiger Lake generation typically lack AVX. Older Intel Xeon models may have AVX, but may lack AVX2.
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
|
|
||||||
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
|
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
|
||||||
|
|
||||||
|
|
||||||
#### SSDs (Solid State Drives)
|
#### SSDs (Solid State Drives)
|
||||||
|
|
||||||
SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs.
|
SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs.
|
||||||
@ -71,4 +74,4 @@ While supported, using network-attached storage (NAS) for recordings can introdu
|
|||||||
|
|
||||||
- **Basic Minimum: 4GB RAM**: This is generally sufficient for a very basic Frigate setup with a few cameras and a dedicated object detection accelerator, without running any enrichments. Performance might be tight, especially with higher resolution streams or numerous detections.
|
- **Basic Minimum: 4GB RAM**: This is generally sufficient for a very basic Frigate setup with a few cameras and a dedicated object detection accelerator, without running any enrichments. Performance might be tight, especially with higher resolution streams or numerous detections.
|
||||||
- **Minimum for Enrichments: 8GB RAM**: If you plan to utilize Frigate's enrichment features (e.g., facial recognition, license plate recognition, or other AI models that run alongside standard object detection), 8GB of RAM should be considered the minimum. Enrichments require additional memory to load and process their respective models and data.
|
- **Minimum for Enrichments: 8GB RAM**: If you plan to utilize Frigate's enrichment features (e.g., facial recognition, license plate recognition, or other AI models that run alongside standard object detection), 8GB of RAM should be considered the minimum. Enrichments require additional memory to load and process their respective models and data.
|
||||||
- **Recommended: 16GB RAM**: For most users, especially those with many cameras (8+) or who plan to heavily leverage enrichments, 16GB of RAM is highly recommended. This provides ample headroom for smooth operation, reduces the likelihood of swapping to disk (which can impact performance), and allows for future expansion.
|
- **Recommended: 16GB RAM**: For most users, especially those with many cameras (8+) or who plan to heavily leverage enrichments, 16GB of RAM is highly recommended. This provides ample headroom for smooth operation, reduces the likelihood of swapping to disk (which can impact performance), and allows for future expansion.
|
||||||
|
|||||||
@ -7,7 +7,7 @@ title: Updating
|
|||||||
|
|
||||||
The current stable version of Frigate is **0.17.0**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.17.0).
|
The current stable version of Frigate is **0.17.0**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.17.0).
|
||||||
|
|
||||||
Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant Addon, etc.). Below are instructions for the most common setups.
|
Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant App, etc.). Below are instructions for the most common setups.
|
||||||
|
|
||||||
## Before You Begin
|
## Before You Begin
|
||||||
|
|
||||||
@ -67,30 +67,30 @@ If you’re running Frigate via Docker (recommended method), follow these steps:
|
|||||||
- If you’ve customized other settings (e.g., `shm-size`), ensure they’re still appropriate after the update.
|
- If you’ve customized other settings (e.g., `shm-size`), ensure they’re still appropriate after the update.
|
||||||
- Docker will automatically use the updated image when you restart the container, as long as you pulled the correct version.
|
- Docker will automatically use the updated image when you restart the container, as long as you pulled the correct version.
|
||||||
|
|
||||||
## Updating the Home Assistant Addon
|
## Updating the Home Assistant App (formerly Addon)
|
||||||
|
|
||||||
For users running Frigate as a Home Assistant Addon:
|
For users running Frigate as a Home Assistant App:
|
||||||
|
|
||||||
1. **Check for Updates**:
|
1. **Check for Updates**:
|
||||||
- Navigate to **Settings > Add-ons** in Home Assistant.
|
- Navigate to **Settings > Apps** in Home Assistant.
|
||||||
- Find your installed Frigate addon (e.g., "Frigate NVR" or "Frigate NVR (Full Access)").
|
- Find your installed Frigate app (e.g., "Frigate NVR" or "Frigate NVR (Full Access)").
|
||||||
- If an update is available, you’ll see an "Update" button.
|
- If an update is available, you’ll see an "Update" button.
|
||||||
|
|
||||||
2. **Update the Addon**:
|
2. **Update the App**:
|
||||||
- Click the "Update" button next to the Frigate addon.
|
- Click the "Update" button next to the Frigate app.
|
||||||
- Wait for the process to complete. Home Assistant will handle downloading and installing the new version.
|
- Wait for the process to complete. Home Assistant will handle downloading and installing the new version.
|
||||||
|
|
||||||
3. **Restart the Addon**:
|
3. **Restart the App**:
|
||||||
- After updating, go to the addon’s page and click "Restart" to apply the changes.
|
- After updating, go to the app’s page and click "Restart" to apply the changes.
|
||||||
|
|
||||||
4. **Verify the Update**:
|
4. **Verify the Update**:
|
||||||
- Check the addon logs (under the "Log" tab) to ensure Frigate starts without errors.
|
- Check the app logs (under the "Log" tab) to ensure Frigate starts without errors.
|
||||||
- Access the Frigate Web UI to confirm the new version is running.
|
- Access the Frigate Web UI to confirm the new version is running.
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
- Ensure your `/config/frigate.yml` is compatible with the new version by reviewing the [Release notes](https://github.com/blakeblackshear/frigate/releases).
|
- Ensure your `/config/frigate.yml` is compatible with the new version by reviewing the [Release notes](https://github.com/blakeblackshear/frigate/releases).
|
||||||
- If using custom hardware (e.g., Coral or GPU), verify that configurations still work, as addon updates don’t modify your hardware settings.
|
- If using custom hardware (e.g., Coral or GPU), verify that configurations still work, as app updates don’t modify your hardware settings.
|
||||||
|
|
||||||
## Rolling Back
|
## Rolling Back
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ If an update causes issues:
|
|||||||
3. Revert to the previous image version:
|
3. Revert to the previous image version:
|
||||||
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`) in your `docker run` command.
|
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`) in your `docker run` command.
|
||||||
- For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`.
|
- For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`.
|
||||||
- For Home Assistant: Reinstall the previous addon version manually via the repository if needed and restart the addon.
|
- For Home Assistant: Restore from the app/addon backup you took before you updated.
|
||||||
4. Verify the old version is running again.
|
4. Verify the old version is running again.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@ -37,18 +37,18 @@ The following diagram adds a lot more detail than the simple view explained befo
|
|||||||
%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%%
|
%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%%
|
||||||
|
|
||||||
flowchart TD
|
flowchart TD
|
||||||
RecStore[(Recording\nstore)]
|
RecStore[(Recording<br>store)]
|
||||||
SnapStore[(Snapshot\nstore)]
|
SnapStore[(Snapshot<br>store)]
|
||||||
|
|
||||||
subgraph Acquisition
|
subgraph Acquisition
|
||||||
Cam["Camera"] -->|FFmpeg supported| Stream
|
Cam["Camera"] -->|FFmpeg supported| Stream
|
||||||
Cam -->|"Other streaming\nprotocols"| go2rtc
|
Cam -->|"Other streaming<br>protocols"| go2rtc
|
||||||
go2rtc("go2rtc") --> Stream
|
go2rtc("go2rtc") --> Stream
|
||||||
Stream[Capture main and\nsub streams] --> |detect stream|Decode(Decode and\ndownscale)
|
Stream[Capture main and<br>sub streams] --> |detect stream|Decode(Decode and<br>downscale)
|
||||||
end
|
end
|
||||||
subgraph Motion
|
subgraph Motion
|
||||||
Decode --> MotionM(Apply\nmotion masks)
|
Decode --> MotionM(Apply<br>motion masks)
|
||||||
MotionM --> MotionD(Motion\ndetection)
|
MotionM --> MotionD(Motion<br>detection)
|
||||||
end
|
end
|
||||||
subgraph Detection
|
subgraph Detection
|
||||||
MotionD --> |motion regions| ObjectD(Object detection)
|
MotionD --> |motion regions| ObjectD(Object detection)
|
||||||
@ -60,8 +60,8 @@ flowchart TD
|
|||||||
MotionD --> |motion event|Birdseye
|
MotionD --> |motion event|Birdseye
|
||||||
ObjectZ --> |object event|Birdseye
|
ObjectZ --> |object event|Birdseye
|
||||||
|
|
||||||
MotionD --> |"video segments\n(retain motion)"|RecStore
|
MotionD --> |"video segments<br>(retain motion)"|RecStore
|
||||||
ObjectZ --> |detection clip|RecStore
|
ObjectZ --> |detection clip|RecStore
|
||||||
Stream -->|"video segments\n(retain all)"| RecStore
|
Stream -->|"video segments<br>(retain all)"| RecStore
|
||||||
ObjectZ --> |detection snapshot|SnapStore
|
ObjectZ --> |detection snapshot|SnapStore
|
||||||
```
|
```
|
||||||
|
|||||||
@ -33,19 +33,16 @@ After adding this to the config, restart Frigate and try to watch the live strea
|
|||||||
### What if my video doesn't play?
|
### What if my video doesn't play?
|
||||||
|
|
||||||
- Check Logs:
|
- Check Logs:
|
||||||
|
|
||||||
- Access the go2rtc logs in the Frigate UI under Logs in the sidebar.
|
- Access 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 go2rtc is having difficulty connecting to your camera, you should see some error messages in the log.
|
||||||
|
|
||||||
- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
|
- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
|
||||||
|
|
||||||
- Navigate to port 1984 in your browser to access go2rtc's web interface.
|
- Navigate to port 1984 in your browser to access go2rtc's web interface.
|
||||||
- If using Frigate through Home Assistant, enable the web interface at port 1984.
|
- If using Frigate through Home Assistant, enable the web interface at port 1984.
|
||||||
- If using Docker, forward port 1984 before accessing the web interface.
|
- If using Docker, forward port 1984 before accessing the web interface.
|
||||||
- Click `stream` for the specific camera to see if the camera's stream is being received.
|
- Click `stream` for the specific camera to see if the camera's stream is being received.
|
||||||
|
|
||||||
- Check Video Codec:
|
- Check Video Codec:
|
||||||
|
|
||||||
- If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
|
- If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
|
||||||
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#codecs-madness) in go2rtc documentation.
|
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#codecs-madness) in go2rtc documentation.
|
||||||
- If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#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.
|
- If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#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.
|
||||||
@ -58,7 +55,6 @@ After adding this to the config, restart Frigate and try to watch the live strea
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Switch to FFmpeg if needed:
|
- Switch to FFmpeg if needed:
|
||||||
|
|
||||||
- Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
|
- Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -101,9 +97,9 @@ After adding this to the config, restart Frigate and try to watch the live strea
|
|||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
To access the go2rtc stream externally when utilizing the Frigate Add-On (for
|
To access the go2rtc stream externally when utilizing the Frigate App (for
|
||||||
instance through VLC), you must first enable the RTSP Restream port.
|
instance through VLC), you must first enable the RTSP Restream port.
|
||||||
You can do this by visiting the Frigate Add-On configuration page within Home
|
You can do this by visiting the Frigate App configuration page within Home
|
||||||
Assistant and revealing the hidden options under the "Show disabled ports"
|
Assistant and revealing the hidden options under the "Show disabled ports"
|
||||||
section.
|
section.
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ title: Getting started
|
|||||||
|
|
||||||
If you already have an environment with Linux and Docker installed, you can continue to [Installing Frigate](#installing-frigate) below.
|
If you already have an environment with Linux and Docker installed, you can continue to [Installing Frigate](#installing-frigate) below.
|
||||||
|
|
||||||
If you already have Frigate installed through Docker or through a Home Assistant Add-on, you can continue to [Configuring Frigate](#configuring-frigate) below.
|
If you already have Frigate installed through Docker or through a Home Assistant App, you can continue to [Configuring Frigate](#configuring-frigate) below.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ Now you have a minimal Debian server that requires very little maintenance.
|
|||||||
|
|
||||||
## Installing Frigate
|
## Installing Frigate
|
||||||
|
|
||||||
This section shows how to create a minimal directory structure for a Docker installation on Debian. If you have installed Frigate as a Home Assistant Add-on or another way, you can continue to [Configuring Frigate](#configuring-frigate).
|
This section shows how to create a minimal directory structure for a Docker installation on Debian. If you have installed Frigate as a Home Assistant App or another way, you can continue to [Configuring Frigate](#configuring-frigate).
|
||||||
|
|
||||||
### Setup directories
|
### Setup directories
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ Here is an example configuration with hardware acceleration configured to work w
|
|||||||
|
|
||||||
`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes)
|
`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes)
|
||||||
|
|
||||||
```yaml
|
```yaml {4,5}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -168,17 +168,57 @@ cameras:
|
|||||||
name_of_your_camera:
|
name_of_your_camera:
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
inputs: ...
|
inputs: ...
|
||||||
|
# highlight-next-line
|
||||||
hwaccel_args: preset-vaapi
|
hwaccel_args: preset-vaapi
|
||||||
detect: ...
|
detect: ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Configure detectors
|
### Step 4: Configure detectors
|
||||||
|
|
||||||
By default, Frigate will use a single CPU detector. If you have a USB Coral, you will need to add a detectors section to your config.
|
By default, Frigate will use a single CPU detector.
|
||||||
|
|
||||||
|
In many cases, the integrated graphics on Intel CPUs provides sufficient performance for typical Frigate setups. If you have an Intel processor, you can follow the configuration below.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Use Intel OpenVINO detector</summary>
|
||||||
|
|
||||||
|
You need to refer to **Configure hardware acceleration** above to enable the container to use the GPU.
|
||||||
|
|
||||||
|
```yaml {3-6,9-15,20-21}
|
||||||
|
mqtt: ...
|
||||||
|
|
||||||
|
detectors: # <---- add detectors
|
||||||
|
ov:
|
||||||
|
type: openvino # <---- use openvino detector
|
||||||
|
device: GPU
|
||||||
|
|
||||||
|
# We will use the default MobileNet_v2 model from OpenVINO.
|
||||||
|
model:
|
||||||
|
width: 300
|
||||||
|
height: 300
|
||||||
|
input_tensor: nhwc
|
||||||
|
input_pixel_format: bgr
|
||||||
|
path: /openvino-model/ssdlite_mobilenet_v2.xml
|
||||||
|
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
name_of_your_camera:
|
||||||
|
ffmpeg: ...
|
||||||
|
detect:
|
||||||
|
enabled: True # <---- turn on detection
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
If you have a USB Coral, you will need to add a detectors section to your config.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Use USB Coral detector</summary>
|
||||||
|
|
||||||
`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes)
|
`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes)
|
||||||
|
|
||||||
```yaml
|
```yaml {4-6}
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
@ -188,7 +228,7 @@ services:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml {3-6,11-12}
|
||||||
mqtt: ...
|
mqtt: ...
|
||||||
|
|
||||||
detectors: # <---- add detectors
|
detectors: # <---- add detectors
|
||||||
@ -204,6 +244,8 @@ cameras:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
More details on available detectors can be found [here](../configuration/object_detectors.md).
|
More details on available detectors can be found [here](../configuration/object_detectors.md).
|
||||||
|
|
||||||
Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they will need to be added according to the [configuration file reference](../configuration/reference.md).
|
Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they will need to be added according to the [configuration file reference](../configuration/reference.md).
|
||||||
@ -222,7 +264,7 @@ Note that motion masks should not be used to mark out areas where you do not wan
|
|||||||
|
|
||||||
Your configuration should look similar to this now.
|
Your configuration should look similar to this now.
|
||||||
|
|
||||||
```yaml
|
```yaml {16-18}
|
||||||
mqtt:
|
mqtt:
|
||||||
enabled: False
|
enabled: False
|
||||||
|
|
||||||
@ -252,7 +294,7 @@ In order to review activity in the Frigate UI, recordings need to be enabled.
|
|||||||
|
|
||||||
To enable recording video, add the `record` role to a stream and enable it in the config. If record is disabled in the config, it won't be possible to enable it in the UI.
|
To enable recording video, add the `record` role to a stream and enable it in the config. If record is disabled in the config, it won't be possible to enable it in the UI.
|
||||||
|
|
||||||
```yaml
|
```yaml {16-17}
|
||||||
mqtt: ...
|
mqtt: ...
|
||||||
|
|
||||||
detectors: ...
|
detectors: ...
|
||||||
|
|||||||
@ -3,7 +3,7 @@ id: ha_network_storage
|
|||||||
title: Home Assistant network storage
|
title: Home Assistant network storage
|
||||||
---
|
---
|
||||||
|
|
||||||
As of Home Assistant 2023.6, Network Mounted Storage is supported for Add-ons.
|
As of Home Assistant 2023.6, Network Mounted Storage is supported for Apps.
|
||||||
|
|
||||||
## Setting Up Remote Storage For Frigate
|
## Setting Up Remote Storage For Frigate
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ As of Home Assistant 2023.6, Network Mounted Storage is supported for Add-ons.
|
|||||||
|
|
||||||
### Initial Setup
|
### Initial Setup
|
||||||
|
|
||||||
1. Stop the Frigate Add-on
|
1. Stop the Frigate App
|
||||||
|
|
||||||
### Move current data
|
### Move current data
|
||||||
|
|
||||||
@ -37,4 +37,4 @@ Keeping the current data is optional, but the data will need to be moved regardl
|
|||||||
4. Fill out the additional required info for your particular NAS
|
4. Fill out the additional required info for your particular NAS
|
||||||
5. Connect
|
5. Connect
|
||||||
6. Move files from `/media/frigate_tmp` to `/media/frigate` if they were kept in previous step
|
6. Move files from `/media/frigate_tmp` to `/media/frigate` if they were kept in previous step
|
||||||
7. Start the Frigate Add-on
|
7. Start the Frigate App
|
||||||
|
|||||||
@ -99,11 +99,11 @@ services:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Home Assistant Add-on
|
### Home Assistant App
|
||||||
|
|
||||||
If you are using Home Assistant Add-on, the URL should be one of the following depending on which Add-on variant you are using. Note that if you are using the Proxy Add-on, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network.
|
If you are using Home Assistant App, the URL should be one of the following depending on which App variant you are using. Note that if you are using the Proxy App, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network.
|
||||||
|
|
||||||
| Add-on Variant | URL |
|
| App Variant | URL |
|
||||||
| -------------------------- | -------------------------------------- |
|
| -------------------------- | -------------------------------------- |
|
||||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||||
|
|||||||
@ -19,11 +19,11 @@ Once logged in, you can generate an API key for Frigate in Settings.
|
|||||||
|
|
||||||
### Set your API key
|
### Set your API key
|
||||||
|
|
||||||
In Frigate, you can use an environment variable or a docker secret named `PLUS_API_KEY` to enable the `Frigate+` buttons on the Explore page. Home Assistant Addon users can set it under Settings > Add-ons > Frigate > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch).
|
In Frigate, you can use an environment variable or a docker secret named `PLUS_API_KEY` to enable the `Frigate+` buttons on the Explore page. Home Assistant App users can set it under Settings > Apps > Frigate > Configuration > Options (be sure to toggle the "Show unused optional configuration options" switch).
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
You cannot use the `environment_vars` section of your Frigate configuration file to set this environment variable. It must be defined as an environment variable in the docker config or Home Assistant Add-on config.
|
You cannot use the `environment_vars` section of your Frigate configuration file to set this environment variable. It must be defined as an environment variable in the docker config or Home Assistant App config.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@ -42,3 +42,7 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht
|
|||||||
## [Scrypted - Frigate bridge plugin](https://github.com/apocaliss92/scrypted-frigate-bridge)
|
## [Scrypted - Frigate bridge plugin](https://github.com/apocaliss92/scrypted-frigate-bridge)
|
||||||
|
|
||||||
[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is an plugin that allows to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate.
|
[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is an plugin that allows to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate.
|
||||||
|
|
||||||
|
## [Strix](https://github.com/eduard256/Strix)
|
||||||
|
|
||||||
|
[Strix](https://github.com/eduard256/Strix) auto-discovers working stream URLs for IP cameras and generates ready-to-use Frigate configs. It tests thousands of URL patterns against your camera and supports cameras without RTSP or ONVIF. 67K+ camera models from 3.6K+ brands.
|
||||||
|
|||||||
@ -32,7 +32,7 @@ The USB coral can draw up to 900mA and this can be too much for some on-device U
|
|||||||
The USB coral has different IDs when it is uninitialized and initialized.
|
The USB coral has different IDs when it is uninitialized and initialized.
|
||||||
|
|
||||||
- When running Frigate in a VM, Proxmox lxc, etc. you must ensure both device IDs are mapped.
|
- When running Frigate in a VM, Proxmox lxc, etc. you must ensure both device IDs are mapped.
|
||||||
- When running through the Home Assistant OS you may need to run the Full Access variant of the Frigate Add-on with the _Protection mode_ switch disabled so that the coral can be accessed.
|
- When running through the Home Assistant OS you may need to run the Full Access variant of the Frigate App with the _Protection mode_ switch disabled so that the coral can be accessed.
|
||||||
|
|
||||||
### Synology 716+II running DSM 7.2.1-69057 Update 5
|
### Synology 716+II running DSM 7.2.1-69057 Update 5
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,17 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
|
magicComments:[
|
||||||
|
{
|
||||||
|
className: 'theme-code-block-highlighted-line',
|
||||||
|
line: 'highlight-next-line',
|
||||||
|
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'code-block-error-line',
|
||||||
|
line: 'highlight-error-line',
|
||||||
|
},
|
||||||
|
],
|
||||||
additionalLanguages: ["bash", "json"],
|
additionalLanguages: ["bash", "json"],
|
||||||
},
|
},
|
||||||
languageTabs: [
|
languageTabs: [
|
||||||
|
|||||||
@ -234,3 +234,11 @@
|
|||||||
content: "schema";
|
content: "schema";
|
||||||
color: var(--ifm-color-secondary-contrast-foreground);
|
color: var(--ifm-color-secondary-contrast-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block-error-line {
|
||||||
|
background-color: #ff000020;
|
||||||
|
display: block;
|
||||||
|
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||||
|
padding: 0 var(--ifm-pre-padding);
|
||||||
|
border-left: 3px solid #ff000080;
|
||||||
|
}
|
||||||
@ -321,7 +321,7 @@ def config_raw_paths(request: Request):
|
|||||||
return JSONResponse(content=raw_paths)
|
return JSONResponse(content=raw_paths)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config/raw", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/config/raw", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def config_raw():
|
def config_raw():
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
|
|
||||||
@ -1073,7 +1073,12 @@ def get_recognized_license_plates(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/timeline", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
def timeline(
|
||||||
|
camera: str = "all",
|
||||||
|
limit: int = 100,
|
||||||
|
source_id: Optional[str] = None,
|
||||||
|
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
|
):
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
selected_columns = [
|
selected_columns = [
|
||||||
@ -1095,6 +1100,9 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N
|
|||||||
else:
|
else:
|
||||||
clauses.append((Timeline.source_id.in_(source_ids)))
|
clauses.append((Timeline.source_id.in_(source_ids)))
|
||||||
|
|
||||||
|
# Enforce per-camera access control
|
||||||
|
clauses.append((Timeline.camera << allowed_cameras))
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append((True))
|
||||||
|
|
||||||
@ -1110,7 +1118,10 @@ def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = N
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
|
def hourly_timeline(
|
||||||
|
params: AppTimelineHourlyQueryParameters = Depends(),
|
||||||
|
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
|
):
|
||||||
"""Get hourly summary for timeline."""
|
"""Get hourly summary for timeline."""
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
labels = params.labels
|
labels = params.labels
|
||||||
@ -1128,6 +1139,9 @@ def hourly_timeline(params: AppTimelineHourlyQueryParameters = Depends()):
|
|||||||
camera_list = cameras.split(",")
|
camera_list = cameras.split(",")
|
||||||
clauses.append((Timeline.camera << camera_list))
|
clauses.append((Timeline.camera << camera_list))
|
||||||
|
|
||||||
|
# Enforce per-camera access control
|
||||||
|
clauses.append((Timeline.camera << allowed_cameras))
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
label_list = labels.split(",")
|
label_list = labels.split(",")
|
||||||
clauses.append((Timeline.data["label"] << label_list))
|
clauses.append((Timeline.data["label"] << label_list))
|
||||||
|
|||||||
@ -73,7 +73,6 @@ def require_admin_by_default():
|
|||||||
"/stats",
|
"/stats",
|
||||||
"/stats/history",
|
"/stats/history",
|
||||||
"/config",
|
"/config",
|
||||||
"/config/raw",
|
|
||||||
"/vainfo",
|
"/vainfo",
|
||||||
"/nvinfo",
|
"/nvinfo",
|
||||||
"/labels",
|
"/labels",
|
||||||
@ -896,6 +895,7 @@ def create_user(
|
|||||||
User.notification_tokens: [],
|
User.notification_tokens: [],
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"username": body.username})
|
return JSONResponse(content={"username": body.username})
|
||||||
|
|
||||||
|
|
||||||
@ -913,6 +913,7 @@ def delete_user(request: Request, username: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
User.delete_by_id(username)
|
User.delete_by_id(username)
|
||||||
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
|
|
||||||
@ -1032,6 +1033,7 @@ async def update_role(
|
|||||||
)
|
)
|
||||||
|
|
||||||
User.set_by_id(username, {User.role: body.role})
|
User.set_by_id(username, {User.role: body.role})
|
||||||
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
|
|
||||||
@ -1045,7 +1047,16 @@ async def require_camera_access(
|
|||||||
|
|
||||||
current_user = await get_current_user(request)
|
current_user = await get_current_user(request)
|
||||||
if isinstance(current_user, JSONResponse):
|
if isinstance(current_user, JSONResponse):
|
||||||
return current_user
|
detail = "Authentication required"
|
||||||
|
try:
|
||||||
|
error_payload = json.loads(current_user.body)
|
||||||
|
detail = (
|
||||||
|
error_payload.get("message") or error_payload.get("detail") or detail
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(status_code=current_user.status_code, detail=detail)
|
||||||
|
|
||||||
role = current_user["role"]
|
role = current_user["role"]
|
||||||
all_camera_names = set(request.app.frigate_config.cameras.keys())
|
all_camera_names = set(request.app.frigate_config.cameras.keys())
|
||||||
@ -1063,6 +1074,61 @@ async def require_camera_access(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
|
||||||
|
owner_cameras: set[str] = set()
|
||||||
|
|
||||||
|
for camera_name, camera in request.app.frigate_config.cameras.items():
|
||||||
|
if stream_name == camera_name:
|
||||||
|
owner_cameras.add(camera_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stream_name in camera.live.streams.values():
|
||||||
|
owner_cameras.add(camera_name)
|
||||||
|
|
||||||
|
return owner_cameras
|
||||||
|
|
||||||
|
|
||||||
|
async def require_go2rtc_stream_access(
|
||||||
|
stream_name: Optional[str] = None,
|
||||||
|
request: Request = None,
|
||||||
|
):
|
||||||
|
"""Dependency to enforce go2rtc stream access based on owning camera access."""
|
||||||
|
if stream_name is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_user = await get_current_user(request)
|
||||||
|
if isinstance(current_user, JSONResponse):
|
||||||
|
detail = "Authentication required"
|
||||||
|
try:
|
||||||
|
error_payload = json.loads(current_user.body)
|
||||||
|
detail = (
|
||||||
|
error_payload.get("message") or error_payload.get("detail") or detail
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise HTTPException(status_code=current_user.status_code, detail=detail)
|
||||||
|
|
||||||
|
role = current_user["role"]
|
||||||
|
all_camera_names = set(request.app.frigate_config.cameras.keys())
|
||||||
|
roles_dict = request.app.frigate_config.auth.roles
|
||||||
|
allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
|
||||||
|
|
||||||
|
# Admin or full access bypasses
|
||||||
|
if role == "admin" or not roles_dict.get(role):
|
||||||
|
return
|
||||||
|
|
||||||
|
owner_cameras = _get_stream_owner_cameras(request, stream_name)
|
||||||
|
|
||||||
|
if owner_cameras & set(allowed_cameras):
|
||||||
|
return
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail=f"Access denied to camera '{stream_name}'. Allowed: {allowed_cameras}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_allowed_cameras_for_filter(request: Request):
|
async def get_allowed_cameras_for_filter(request: Request):
|
||||||
"""Dependency to get allowed_cameras for filtering lists."""
|
"""Dependency to get allowed_cameras for filtering lists."""
|
||||||
current_user = await get_current_user(request)
|
current_user = await get_current_user(request)
|
||||||
|
|||||||
@ -20,7 +20,7 @@ from zeep.transports import AsyncTransport
|
|||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import (
|
||||||
allow_any_authenticated,
|
allow_any_authenticated,
|
||||||
require_camera_access,
|
require_go2rtc_stream_access,
|
||||||
require_role,
|
require_role,
|
||||||
)
|
)
|
||||||
from frigate.api.defs.request.app_body import CameraSetBody
|
from frigate.api.defs.request.app_body import CameraSetBody
|
||||||
@ -81,14 +81,27 @@ def go2rtc_streams():
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)]
|
"/go2rtc/streams/{stream_name}",
|
||||||
|
dependencies=[Depends(require_go2rtc_stream_access)],
|
||||||
)
|
)
|
||||||
def go2rtc_camera_stream(request: Request, camera_name: str):
|
def go2rtc_camera_stream(request: Request, stream_name: str):
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=allµphone"
|
"http://127.0.0.1:1984/api/streams",
|
||||||
|
params={
|
||||||
|
"src": stream_name,
|
||||||
|
"video": "all",
|
||||||
|
"audio": "all",
|
||||||
|
"microphone": "",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
camera_config = request.app.frigate_config.cameras.get(camera_name)
|
camera_config = request.app.frigate_config.cameras.get(stream_name)
|
||||||
|
|
||||||
|
if camera_config is None:
|
||||||
|
for camera_name, camera in request.app.frigate_config.cameras.items():
|
||||||
|
if stream_name in camera.live.streams.values():
|
||||||
|
camera_config = request.app.frigate_config.cameras.get(camera_name)
|
||||||
|
break
|
||||||
|
|
||||||
if camera_config and camera_config.enabled:
|
if camera_config and camera_config.enabled:
|
||||||
logger.error("Failed to fetch streams from go2rtc")
|
logger.error("Failed to fetch streams from go2rtc")
|
||||||
|
|||||||
@ -52,9 +52,11 @@ from frigate.util.file import (
|
|||||||
load_event_snapshot_image,
|
load_event_snapshot_image,
|
||||||
)
|
)
|
||||||
from frigate.util.image import get_image_from_recording, get_image_quality_params
|
from frigate.util.image import get_image_from_recording, get_image_quality_params
|
||||||
|
from frigate.util.media import get_keyframe_before
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=[Tags.media])
|
router = APIRouter(tags=[Tags.media])
|
||||||
|
|
||||||
|
|
||||||
@ -608,6 +610,33 @@ async def vod_ts(
|
|||||||
if recording.end_time > end_ts:
|
if recording.end_time > end_ts:
|
||||||
duration -= int((recording.end_time - end_ts) * 1000)
|
duration -= int((recording.end_time - end_ts) * 1000)
|
||||||
|
|
||||||
|
# nginx-vod-module pushes clipFrom forward to the next keyframe,
|
||||||
|
# which can leave too few frames and produce an empty/unplayable
|
||||||
|
# segment. Snap clipFrom back to the preceding keyframe so the
|
||||||
|
# segment always starts with a decodable frame.
|
||||||
|
if "clipFrom" in clip:
|
||||||
|
keyframe_ms = get_keyframe_before(recording.path, clip["clipFrom"])
|
||||||
|
if keyframe_ms is not None:
|
||||||
|
gained = clip["clipFrom"] - keyframe_ms
|
||||||
|
clip["clipFrom"] = keyframe_ms
|
||||||
|
duration += gained
|
||||||
|
logger.debug(
|
||||||
|
"VOD: snapped clipFrom to keyframe at %sms for %s, duration now %sms",
|
||||||
|
keyframe_ms,
|
||||||
|
recording.path,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# could not read keyframes, remove clipFrom to use full recording
|
||||||
|
logger.debug(
|
||||||
|
"VOD: no keyframe info for %s, removing clipFrom to use full recording",
|
||||||
|
recording.path,
|
||||||
|
)
|
||||||
|
del clip["clipFrom"]
|
||||||
|
duration = int(recording.duration * 1000)
|
||||||
|
if recording.end_time > end_ts:
|
||||||
|
duration -= int((recording.end_time - end_ts) * 1000)
|
||||||
|
|
||||||
if duration < min_duration_ms:
|
if duration < min_duration_ms:
|
||||||
# skip if the clip has no valid duration (too short to contain frames)
|
# skip if the clip has no valid duration (too short to contain frames)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -837,7 +866,6 @@ async def event_snapshot(
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/thumbnail.{extension}",
|
"/events/{event_id}/thumbnail.{extension}",
|
||||||
dependencies=[Depends(require_camera_access)],
|
|
||||||
)
|
)
|
||||||
async def event_thumbnail(
|
async def event_thumbnail(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -881,11 +909,12 @@ async def event_thumbnail(
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
img_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
|
||||||
|
img = cv2.imdecode(img_as_np, flags=1)
|
||||||
|
|
||||||
# android notifications prefer a 2:1 ratio
|
# android notifications prefer a 2:1 ratio
|
||||||
if format == "android":
|
if format == "android":
|
||||||
img_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
|
img = cv2.copyMakeBorder(
|
||||||
img = cv2.imdecode(img_as_np, flags=1)
|
|
||||||
thumbnail = cv2.copyMakeBorder(
|
|
||||||
img,
|
img,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@ -895,12 +924,14 @@ async def event_thumbnail(
|
|||||||
(0, 0, 0),
|
(0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, img = cv2.imencode(
|
quality_params = None
|
||||||
f".{extension.value}",
|
if extension in (Extension.jpg, Extension.jpeg):
|
||||||
thumbnail,
|
quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), 70]
|
||||||
get_image_quality_params(extension.value, None),
|
elif extension == Extension.webp:
|
||||||
)
|
quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60]
|
||||||
thumbnail_bytes = img.tobytes()
|
|
||||||
|
_, encoded = cv2.imencode(f".{extension.value}", img, quality_params)
|
||||||
|
thumbnail_bytes = encoded.tobytes()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
thumbnail_bytes,
|
thumbnail_bytes,
|
||||||
@ -1053,14 +1084,14 @@ def clear_region_grid(request: Request, camera_name: str):
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/snapshot-clean.webp",
|
"/events/{event_id}/snapshot-clean.webp",
|
||||||
dependencies=[Depends(require_camera_access)],
|
|
||||||
)
|
)
|
||||||
def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
|
async def event_snapshot_clean(request: Request, event_id: str, download: bool = False):
|
||||||
webp_bytes = None
|
webp_bytes = None
|
||||||
event_complete = False
|
event_complete = False
|
||||||
try:
|
try:
|
||||||
event = Event.get(Event.id == event_id)
|
event = Event.get(Event.id == event_id)
|
||||||
event_complete = event.end_time is not None
|
event_complete = event.end_time is not None
|
||||||
|
await require_camera_access(event.camera, request=request)
|
||||||
snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots
|
snapshot_config = request.app.frigate_config.cameras[event.camera].snapshots
|
||||||
if not (snapshot_config.enabled and event.has_snapshot):
|
if not (snapshot_config.enabled and event.has_snapshot):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1165,7 +1196,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/clip.mp4", dependencies=[Depends(require_camera_access)]
|
"/events/{event_id}/clip.mp4",
|
||||||
)
|
)
|
||||||
async def event_clip(
|
async def event_clip(
|
||||||
request: Request,
|
request: Request,
|
||||||
@ -1179,6 +1210,8 @@ async def event_clip(
|
|||||||
content={"success": False, "message": "Event not found"}, status_code=404
|
content={"success": False, "message": "Event not found"}, status_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await require_camera_access(event.camera, request=request)
|
||||||
|
|
||||||
if not event.has_clip:
|
if not event.has_clip:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Clip not available"}, status_code=404
|
content={"success": False, "message": "Clip not available"}, status_code=404
|
||||||
@ -1195,9 +1228,9 @@ async def event_clip(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/events/{event_id}/preview.gif", dependencies=[Depends(require_camera_access)]
|
"/events/{event_id}/preview.gif",
|
||||||
)
|
)
|
||||||
def event_preview(request: Request, event_id: str):
|
async def event_preview(request: Request, event_id: str):
|
||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == event_id)
|
event: Event = Event.get(Event.id == event_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
@ -1205,6 +1238,8 @@ def event_preview(request: Request, event_id: str):
|
|||||||
content={"success": False, "message": "Event not found"}, status_code=404
|
content={"success": False, "message": "Event not found"}, status_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await require_camera_access(event.camera, request=request)
|
||||||
|
|
||||||
start_ts = event.start_time
|
start_ts = event.start_time
|
||||||
end_ts = start_ts + (
|
end_ts = start_ts + (
|
||||||
min(event.end_time - event.start_time, 20) if event.end_time else 20
|
min(event.end_time - event.start_time, 20) if event.end_time else 20
|
||||||
@ -1227,25 +1262,25 @@ def preview_gif(
|
|||||||
):
|
):
|
||||||
if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0):
|
if datetime.fromtimestamp(start_ts) < datetime.now().replace(minute=0, second=0):
|
||||||
# has preview mp4
|
# has preview mp4
|
||||||
preview: Previews = (
|
try:
|
||||||
Previews.select(
|
preview: Previews = (
|
||||||
Previews.camera,
|
Previews.select(
|
||||||
Previews.path,
|
Previews.camera,
|
||||||
Previews.duration,
|
Previews.path,
|
||||||
Previews.start_time,
|
Previews.duration,
|
||||||
Previews.end_time,
|
Previews.start_time,
|
||||||
|
Previews.end_time,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Previews.start_time.between(start_ts, end_ts)
|
||||||
|
| Previews.end_time.between(start_ts, end_ts)
|
||||||
|
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
||||||
|
)
|
||||||
|
.where(Previews.camera == camera_name)
|
||||||
|
.limit(1)
|
||||||
|
.get()
|
||||||
)
|
)
|
||||||
.where(
|
except DoesNotExist:
|
||||||
Previews.start_time.between(start_ts, end_ts)
|
|
||||||
| Previews.end_time.between(start_ts, end_ts)
|
|
||||||
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
|
|
||||||
)
|
|
||||||
.where(Previews.camera == camera_name)
|
|
||||||
.limit(1)
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not preview:
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Preview not found"},
|
content={"success": False, "message": "Preview not found"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
@ -1563,8 +1598,8 @@ def preview_mp4(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/review/{event_id}/preview", dependencies=[Depends(require_camera_access)])
|
@router.get("/review/{event_id}/preview")
|
||||||
def review_preview(
|
async def review_preview(
|
||||||
request: Request,
|
request: Request,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
format: str = Query(default="gif", enum=["gif", "mp4"]),
|
format: str = Query(default="gif", enum=["gif", "mp4"]),
|
||||||
@ -1577,6 +1612,8 @@ def review_preview(
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await require_camera_access(review.camera, request=request)
|
||||||
|
|
||||||
padding = 8
|
padding = 8
|
||||||
start_ts = review.start_time - padding
|
start_ts = review.start_time - padding
|
||||||
end_ts = (
|
end_ts = (
|
||||||
@ -1590,12 +1627,14 @@ def review_preview(
|
|||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/preview/{file_name}/thumbnail.jpg", dependencies=[Depends(require_camera_access)]
|
"/preview/{file_name}/thumbnail.jpg",
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
)
|
)
|
||||||
@router.get(
|
@router.get(
|
||||||
"/preview/{file_name}/thumbnail.webp", dependencies=[Depends(require_camera_access)]
|
"/preview/{file_name}/thumbnail.webp",
|
||||||
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
)
|
)
|
||||||
def preview_thumbnail(file_name: str):
|
async def preview_thumbnail(request: Request, file_name: str):
|
||||||
"""Get a thumbnail from the cached preview frames."""
|
"""Get a thumbnail from the cached preview frames."""
|
||||||
if len(file_name) > 1000:
|
if len(file_name) > 1000:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1605,6 +1644,17 @@ def preview_thumbnail(file_name: str):
|
|||||||
status_code=403,
|
status_code=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Extract camera name from preview filename (format: preview_{camera}-{timestamp}.ext)
|
||||||
|
if not file_name.startswith("preview_"):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Invalid preview filename"},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
# Use rsplit to handle camera names containing dashes (e.g. front-door)
|
||||||
|
name_part = file_name[len("preview_") :].rsplit(".", 1)[0] # strip extension
|
||||||
|
camera_name = name_part.rsplit("-", 1)[0] # split off timestamp
|
||||||
|
await require_camera_access(camera_name, request=request)
|
||||||
|
|
||||||
safe_file_name_current = sanitize_filename(file_name)
|
safe_file_name_current = sanitize_filename(file_name)
|
||||||
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from titlecase import titlecase
|
|||||||
from frigate.comms.base_communicator import Communicator
|
from frigate.comms.base_communicator import Communicator
|
||||||
from frigate.comms.config_updater import ConfigSubscriber
|
from frigate.comms.config_updater import ConfigSubscriber
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.config.auth import AuthConfig
|
||||||
from frigate.config.camera.updater import (
|
from frigate.config.camera.updater import (
|
||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
CameraConfigUpdateSubscriber,
|
CameraConfigUpdateSubscriber,
|
||||||
@ -58,6 +59,7 @@ class WebPushClient(Communicator):
|
|||||||
for c in self.config.cameras.values()
|
for c in self.config.cameras.values()
|
||||||
}
|
}
|
||||||
self.last_notification_time: float = 0
|
self.last_notification_time: float = 0
|
||||||
|
self.user_cameras: dict[str, set[str]] = {}
|
||||||
self.notification_queue: queue.Queue[PushNotification] = queue.Queue()
|
self.notification_queue: queue.Queue[PushNotification] = queue.Queue()
|
||||||
self.notification_thread = threading.Thread(
|
self.notification_thread = threading.Thread(
|
||||||
target=self._process_notifications, daemon=True
|
target=self._process_notifications, daemon=True
|
||||||
@ -78,13 +80,12 @@ class WebPushClient(Communicator):
|
|||||||
for sub in user["notification_tokens"]:
|
for sub in user["notification_tokens"]:
|
||||||
self.web_pushers[user["username"]].append(WebPusher(sub))
|
self.web_pushers[user["username"]].append(WebPusher(sub))
|
||||||
|
|
||||||
# notification config updater
|
# notification and auth config updater
|
||||||
self.global_config_subscriber = ConfigSubscriber(
|
self.global_config_subscriber = ConfigSubscriber("config/")
|
||||||
"config/notifications", exact=True
|
|
||||||
)
|
|
||||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||||
self.config, self.config.cameras, [CameraConfigUpdateEnum.notifications]
|
self.config, self.config.cameras, [CameraConfigUpdateEnum.notifications]
|
||||||
)
|
)
|
||||||
|
self._refresh_user_cameras()
|
||||||
|
|
||||||
def subscribe(self, receiver: Callable) -> None:
|
def subscribe(self, receiver: Callable) -> None:
|
||||||
"""Wrapper for allowing dispatcher to subscribe."""
|
"""Wrapper for allowing dispatcher to subscribe."""
|
||||||
@ -164,13 +165,19 @@ class WebPushClient(Communicator):
|
|||||||
|
|
||||||
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
||||||
"""Wrapper for publishing when client is in valid state."""
|
"""Wrapper for publishing when client is in valid state."""
|
||||||
# check for updated notification config
|
# check for updated global config (notifications, auth)
|
||||||
_, updated_notification_config = (
|
while True:
|
||||||
self.global_config_subscriber.check_for_update()
|
config_topic, config_payload = (
|
||||||
)
|
self.global_config_subscriber.check_for_update()
|
||||||
|
)
|
||||||
if updated_notification_config:
|
if config_topic is None:
|
||||||
self.config.notifications = updated_notification_config
|
break
|
||||||
|
if config_topic == "config/notifications" and config_payload:
|
||||||
|
self.config.notifications = config_payload
|
||||||
|
elif config_topic == "config/auth":
|
||||||
|
if isinstance(config_payload, AuthConfig):
|
||||||
|
self.config.auth = config_payload
|
||||||
|
self._refresh_user_cameras()
|
||||||
|
|
||||||
updates = self.config_subscriber.check_for_updates()
|
updates = self.config_subscriber.check_for_updates()
|
||||||
|
|
||||||
@ -300,6 +307,31 @@ class WebPushClient(Communicator):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing notification: {str(e)}")
|
logger.error(f"Error processing notification: {str(e)}")
|
||||||
|
|
||||||
|
def _refresh_user_cameras(self) -> None:
|
||||||
|
"""Rebuild the user-to-cameras access cache from the database."""
|
||||||
|
all_camera_names = set(self.config.cameras.keys())
|
||||||
|
roles_dict = self.config.auth.roles
|
||||||
|
updated: dict[str, set[str]] = {}
|
||||||
|
for user in User.select(User.username, User.role).dicts().iterator():
|
||||||
|
allowed = User.get_allowed_cameras(
|
||||||
|
user["role"], roles_dict, all_camera_names
|
||||||
|
)
|
||||||
|
updated[user["username"]] = set(allowed)
|
||||||
|
logger.debug(
|
||||||
|
"User %s has access to cameras: %s",
|
||||||
|
user["username"],
|
||||||
|
", ".join(allowed),
|
||||||
|
)
|
||||||
|
self.user_cameras = updated
|
||||||
|
|
||||||
|
def _user_has_camera_access(self, username: str, camera: str) -> bool:
|
||||||
|
"""Check if a user has access to a specific camera based on cached roles."""
|
||||||
|
allowed = self.user_cameras.get(username)
|
||||||
|
if allowed is None:
|
||||||
|
logger.debug(f"No camera access information found for user {username}")
|
||||||
|
return False
|
||||||
|
return camera in allowed
|
||||||
|
|
||||||
def _within_cooldown(self, camera: str) -> bool:
|
def _within_cooldown(self, camera: str) -> bool:
|
||||||
now = datetime.datetime.now().timestamp()
|
now = datetime.datetime.now().timestamp()
|
||||||
if now - self.last_notification_time < self.config.notifications.cooldown:
|
if now - self.last_notification_time < self.config.notifications.cooldown:
|
||||||
@ -427,6 +459,14 @@ class WebPushClient(Communicator):
|
|||||||
logger.debug(f"Sending push notification for {camera}, review ID {reviewId}")
|
logger.debug(f"Sending push notification for {camera}, review ID {reviewId}")
|
||||||
|
|
||||||
for user in self.web_pushers:
|
for user in self.web_pushers:
|
||||||
|
if not self._user_has_camera_access(user, camera):
|
||||||
|
logger.debug(
|
||||||
|
"Skipping notification for user %s - no access to camera %s",
|
||||||
|
user,
|
||||||
|
camera,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
self.send_push_notification(
|
self.send_push_notification(
|
||||||
user=user,
|
user=user,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
@ -474,6 +514,14 @@ class WebPushClient(Communicator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for user in self.web_pushers:
|
for user in self.web_pushers:
|
||||||
|
if not self._user_has_camera_access(user, camera):
|
||||||
|
logger.debug(
|
||||||
|
"Skipping notification for user %s - no access to camera %s",
|
||||||
|
user,
|
||||||
|
camera,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
self.send_push_notification(
|
self.send_push_notification(
|
||||||
user=user,
|
user=user,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
|
|||||||
@ -92,7 +92,7 @@ class PtzAutotrackConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class OnvifConfig(FrigateBaseModel):
|
class OnvifConfig(FrigateBaseModel):
|
||||||
host: str = Field(
|
host: EnvString = Field(
|
||||||
default="",
|
default="",
|
||||||
title="ONVIF host",
|
title="ONVIF host",
|
||||||
description="Host (and optional scheme) for the ONVIF service for this camera.",
|
description="Host (and optional scheme) for the ONVIF service for this camera.",
|
||||||
|
|||||||
@ -24,8 +24,10 @@ EnvString = Annotated[str, AfterValidator(validate_env_string)]
|
|||||||
|
|
||||||
def validate_env_vars(v: dict[str, str], info: ValidationInfo) -> dict[str, str]:
|
def validate_env_vars(v: dict[str, str], info: ValidationInfo) -> dict[str, str]:
|
||||||
if isinstance(info.context, dict) and info.context.get("install", False):
|
if isinstance(info.context, dict) and info.context.get("install", False):
|
||||||
for k, v in v.items():
|
for k, val in v.items():
|
||||||
os.environ[k] = v
|
os.environ[k] = val
|
||||||
|
if k.startswith("FRIGATE_"):
|
||||||
|
FRIGATE_ENV_VARS[k] = val
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class MqttConfig(FrigateBaseModel):
|
|||||||
title="Enable MQTT",
|
title="Enable MQTT",
|
||||||
description="Enable or disable MQTT integration for state, events, and snapshots.",
|
description="Enable or disable MQTT integration for state, events, and snapshots.",
|
||||||
)
|
)
|
||||||
host: str = Field(
|
host: EnvString = Field(
|
||||||
default="",
|
default="",
|
||||||
title="MQTT host",
|
title="MQTT host",
|
||||||
description="Hostname or IP address of the MQTT broker.",
|
description="Hostname or IP address of the MQTT broker.",
|
||||||
|
|||||||
@ -103,16 +103,19 @@ class ObjectDescriptionProcessor(PostProcessorApi):
|
|||||||
logger.debug(f"{camera} sending early request to GenAI")
|
logger.debug(f"{camera} sending early request to GenAI")
|
||||||
|
|
||||||
self.early_request_sent[data["id"]] = True
|
self.early_request_sent[data["id"]] = True
|
||||||
|
# Copy thumbnails to avoid holding references after cleanup
|
||||||
|
thumbnails_copy = [
|
||||||
|
data["thumbnail"][:] if data.get("thumbnail") else None
|
||||||
|
for data in self.tracked_events[data["id"]]
|
||||||
|
if data.get("thumbnail")
|
||||||
|
]
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._genai_embed_description,
|
target=self._genai_embed_description,
|
||||||
name=f"_genai_embed_description_{event.id}",
|
name=f"_genai_embed_description_{event.id}",
|
||||||
daemon=True,
|
daemon=True,
|
||||||
args=(
|
args=(
|
||||||
event,
|
event,
|
||||||
[
|
thumbnails_copy,
|
||||||
data["thumbnail"]
|
|
||||||
for data in self.tracked_events[data["id"]]
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
@ -172,8 +175,13 @@ class ObjectDescriptionProcessor(PostProcessorApi):
|
|||||||
embed_image = (
|
embed_image = (
|
||||||
[snapshot_image]
|
[snapshot_image]
|
||||||
if event.has_snapshot and source == "snapshot"
|
if event.has_snapshot and source == "snapshot"
|
||||||
|
# Copy thumbnails to avoid holding references
|
||||||
else (
|
else (
|
||||||
[data["thumbnail"] for data in self.tracked_events[event_id]]
|
[
|
||||||
|
data["thumbnail"][:] if data.get("thumbnail") else None
|
||||||
|
for data in self.tracked_events[event_id]
|
||||||
|
if data.get("thumbnail")
|
||||||
|
]
|
||||||
if len(self.tracked_events.get(event_id, [])) > 0
|
if len(self.tracked_events.get(event_id, [])) > 0
|
||||||
else [thumbnail]
|
else [thumbnail]
|
||||||
)
|
)
|
||||||
@ -265,8 +273,13 @@ class ObjectDescriptionProcessor(PostProcessorApi):
|
|||||||
embed_image = (
|
embed_image = (
|
||||||
[snapshot_image]
|
[snapshot_image]
|
||||||
if event.has_snapshot and camera_config.objects.genai.use_snapshot
|
if event.has_snapshot and camera_config.objects.genai.use_snapshot
|
||||||
|
# Copy thumbnails to avoid holding references after cleanup
|
||||||
else (
|
else (
|
||||||
[data["thumbnail"] for data in self.tracked_events[event.id]]
|
[
|
||||||
|
data["thumbnail"][:] if data.get("thumbnail") else None
|
||||||
|
for data in self.tracked_events[event.id]
|
||||||
|
if data.get("thumbnail")
|
||||||
|
]
|
||||||
if num_thumbnails > 0
|
if num_thumbnails > 0
|
||||||
else [thumbnail]
|
else [thumbnail]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -463,6 +463,13 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
thumbs = []
|
thumbs = []
|
||||||
for idx, thumb_path in enumerate(frame_paths):
|
for idx, thumb_path in enumerate(frame_paths):
|
||||||
thumb_data = cv2.imread(thumb_path)
|
thumb_data = cv2.imread(thumb_path)
|
||||||
|
|
||||||
|
if thumb_data is None:
|
||||||
|
logger.warning(
|
||||||
|
"Could not read preview frame at %s, skipping", thumb_path
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
ret, jpg = cv2.imencode(
|
ret, jpg = cv2.imencode(
|
||||||
".jpg", thumb_data, [int(cv2.IMWRITE_JPEG_QUALITY), 100]
|
".jpg", thumb_data, [int(cv2.IMWRITE_JPEG_QUALITY), 100]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -527,6 +527,17 @@ class RKNNModelRunner(BaseModelRunner):
|
|||||||
# Transpose from NCHW to NHWC
|
# Transpose from NCHW to NHWC
|
||||||
pixel_data = np.transpose(pixel_data, (0, 2, 3, 1))
|
pixel_data = np.transpose(pixel_data, (0, 2, 3, 1))
|
||||||
rknn_inputs.append(pixel_data)
|
rknn_inputs.append(pixel_data)
|
||||||
|
elif name == "data":
|
||||||
|
# ArcFace: undo Python normalisation to uint8 [0,255]
|
||||||
|
# RKNN runtime applies mean=127.5/std=127.5 internally before first layer
|
||||||
|
face_data = inputs[name]
|
||||||
|
if len(face_data.shape) == 4 and face_data.shape[1] == 3:
|
||||||
|
# Transpose from NCHW to NHWC
|
||||||
|
face_data = np.transpose(face_data, (0, 2, 3, 1))
|
||||||
|
face_data = (
|
||||||
|
((face_data + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
|
||||||
|
)
|
||||||
|
rknn_inputs.append(face_data)
|
||||||
else:
|
else:
|
||||||
rknn_inputs.append(inputs[name])
|
rknn_inputs.append(inputs[name])
|
||||||
|
|
||||||
|
|||||||
@ -705,4 +705,7 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
if not self.config.semantic_search.enabled:
|
if not self.config.semantic_search.enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.embeddings.embed_thumbnail(event_id, thumbnail)
|
try:
|
||||||
|
self.embeddings.embed_thumbnail(event_id, thumbnail)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Failed to embed thumbnail for event {event_id}")
|
||||||
|
|||||||
@ -321,6 +321,9 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
self.start_or_restart_ffmpeg()
|
self.start_or_restart_ffmpeg()
|
||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
|
# check if there is an updated config
|
||||||
|
self.config_subscriber.check_for_updates()
|
||||||
|
|
||||||
enabled = self.camera_config.enabled
|
enabled = self.camera_config.enabled
|
||||||
if enabled != self.was_enabled:
|
if enabled != self.was_enabled:
|
||||||
if enabled:
|
if enabled:
|
||||||
@ -347,9 +350,6 @@ class AudioEventMaintainer(threading.Thread):
|
|||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check if there is an updated config
|
|
||||||
self.config_subscriber.check_for_updates()
|
|
||||||
|
|
||||||
self.read_audio()
|
self.read_audio()
|
||||||
|
|
||||||
if self.audio_listener:
|
if self.audio_listener:
|
||||||
|
|||||||
@ -326,6 +326,10 @@ class EventCleanup(threading.Thread):
|
|||||||
return events_to_update
|
return events_to_update
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
if self.config.safe_mode:
|
||||||
|
logger.info("Safe mode enabled, skipping event cleanup")
|
||||||
|
return
|
||||||
|
|
||||||
# only expire events every 5 minutes
|
# only expire events every 5 minutes
|
||||||
while not self.stop_event.wait(300):
|
while not self.stop_event.wait(300):
|
||||||
events_with_expired_clips = self.expire_clips()
|
events_with_expired_clips = self.expire_clips()
|
||||||
|
|||||||
@ -368,6 +368,11 @@ class RecordingCleanup(threading.Thread):
|
|||||||
return maybe_empty_dirs
|
return maybe_empty_dirs
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
|
||||||
|
if self.config.safe_mode:
|
||||||
|
logger.info("Safe mode enabled, skipping recording cleanup")
|
||||||
|
return
|
||||||
|
|
||||||
# Expire tmp clips every minute, recordings and clean directories every hour.
|
# Expire tmp clips every minute, recordings and clean directories every hour.
|
||||||
for counter in itertools.cycle(range(self.config.record.expire_interval)):
|
for counter in itertools.cycle(range(self.config.record.expire_interval)):
|
||||||
if self.stop_event.wait(60):
|
if self.stop_event.wait(60):
|
||||||
|
|||||||
@ -280,6 +280,10 @@ class StorageMaintainer(threading.Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Check every 5 minutes if storage needs to be cleaned up."""
|
"""Check every 5 minutes if storage needs to be cleaned up."""
|
||||||
|
if self.config.safe_mode:
|
||||||
|
logger.info("Safe mode enabled, skipping storage maintenance")
|
||||||
|
return
|
||||||
|
|
||||||
self.calculate_camera_bandwidth()
|
self.calculate_camera_bandwidth()
|
||||||
while not self.stop_event.wait(300):
|
while not self.stop_event.wait(300):
|
||||||
if not self.camera_storage_stats or True in [
|
if not self.camera_storage_stats or True in [
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from frigate.api.auth import (
|
from frigate.api.auth import (
|
||||||
get_allowed_cameras_for_filter,
|
get_allowed_cameras_for_filter,
|
||||||
@ -9,6 +10,33 @@ from frigate.api.auth import (
|
|||||||
from frigate.models import Event, Recordings, ReviewSegment
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
||||||
|
|
||||||
|
# Minimal multi-camera config used by go2rtc stream access tests.
|
||||||
|
# front_door has a stream alias "front_door_main"; back_door uses its own name.
|
||||||
|
# The "limited_user" role is restricted to front_door only.
|
||||||
|
_MULTI_CAMERA_CONFIG = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"auth": {
|
||||||
|
"roles": {
|
||||||
|
"limited_user": ["front_door"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"front_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
"live": {"streams": {"default": "front_door_main"}},
|
||||||
|
},
|
||||||
|
"back_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestCameraAccessEventReview(BaseTestHttp):
|
class TestCameraAccessEventReview(BaseTestHttp):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -190,3 +218,179 @@ class TestCameraAccessEventReview(BaseTestHttp):
|
|||||||
resp = client.get("/events/summary")
|
resp = client.get("/events/summary")
|
||||||
summary_list = resp.json()
|
summary_list = resp.json()
|
||||||
assert len(summary_list) == 2
|
assert len(summary_list) == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestGo2rtcStreamAccess(BaseTestHttp):
|
||||||
|
"""Tests for require_go2rtc_stream_access — the auth dependency on
|
||||||
|
GET /go2rtc/streams/{stream_name}.
|
||||||
|
|
||||||
|
go2rtc is not running in unit tests, so an authorized request returns
|
||||||
|
500 (the proxy call fails), while an unauthorized request returns 401/403
|
||||||
|
before the proxy is ever reached.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _make_app(self, config_override: dict | None = None):
|
||||||
|
"""Build a test app, optionally replacing self.minimal_config."""
|
||||||
|
if config_override is not None:
|
||||||
|
self.minimal_config = config_override
|
||||||
|
app = super().create_app()
|
||||||
|
|
||||||
|
# Allow tests to control the current user via request headers.
|
||||||
|
async def mock_get_current_user(request: Request):
|
||||||
|
username = request.headers.get("remote-user")
|
||||||
|
role = request.headers.get("remote-role")
|
||||||
|
if not username or not role:
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={"message": "No authorization headers."},
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
return {"username": username, "role": role}
|
||||||
|
|
||||||
|
app.dependency_overrides[get_current_user] = mock_get_current_user
|
||||||
|
return app
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp([Event, ReviewSegment, Recordings])
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _get_stream(
|
||||||
|
self, app, stream_name: str, role: str = "admin", user: str = "test"
|
||||||
|
):
|
||||||
|
"""Issue GET /go2rtc/streams/{stream_name} with the given role."""
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
return client.get(
|
||||||
|
f"/go2rtc/streams/{stream_name}",
|
||||||
|
headers={"remote-user": user, "remote-role": role},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_admin_can_access_any_stream(self):
|
||||||
|
"""Admin role bypasses camera restrictions."""
|
||||||
|
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
||||||
|
# front_door stream — go2rtc is not running so expect 500, not 401/403
|
||||||
|
resp = self._get_stream(app, "front_door", role="admin")
|
||||||
|
assert resp.status_code not in (401, 403), (
|
||||||
|
f"Admin should not be blocked; got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# back_door stream
|
||||||
|
resp = self._get_stream(app, "back_door", role="admin")
|
||||||
|
assert resp.status_code not in (401, 403)
|
||||||
|
|
||||||
|
def test_missing_auth_headers_returns_401(self):
|
||||||
|
"""Requests without auth headers must be rejected with 401."""
|
||||||
|
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
||||||
|
# Use plain TestClient (not AuthTestClient) so no headers are injected.
|
||||||
|
with TestClient(app, raise_server_exceptions=False) as client:
|
||||||
|
resp = client.get("/go2rtc/streams/front_door")
|
||||||
|
assert resp.status_code == 401, f"Expected 401, got {resp.status_code}"
|
||||||
|
|
||||||
|
def test_unconfigured_role_can_access_any_stream(self):
|
||||||
|
"""When no camera restrictions are configured for a role the user
|
||||||
|
should have access to all streams (no roles_dict entry ⇒ no restriction)."""
|
||||||
|
no_roles_config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"cameras": {
|
||||||
|
"front_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
},
|
||||||
|
"back_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app = self._make_app(no_roles_config)
|
||||||
|
|
||||||
|
# "myuser" role is not listed in roles_dict — should be allowed everywhere
|
||||||
|
for stream in ("front_door", "back_door"):
|
||||||
|
resp = self._get_stream(app, stream, role="myuser")
|
||||||
|
assert resp.status_code not in (401, 403), (
|
||||||
|
f"Unconfigured role should not be blocked on '{stream}'; "
|
||||||
|
f"got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_restricted_role_can_access_allowed_camera(self):
|
||||||
|
"""limited_user role (restricted to front_door) can access front_door stream."""
|
||||||
|
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
||||||
|
resp = self._get_stream(app, "front_door", role="limited_user")
|
||||||
|
assert resp.status_code not in (401, 403), (
|
||||||
|
f"limited_user should be allowed on front_door; got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_restricted_role_blocked_from_disallowed_camera(self):
|
||||||
|
"""limited_user role (restricted to front_door) cannot access back_door stream."""
|
||||||
|
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
||||||
|
resp = self._get_stream(app, "back_door", role="limited_user")
|
||||||
|
assert resp.status_code == 403, (
|
||||||
|
f"limited_user should be denied on back_door; got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_alias_allowed_for_owning_camera(self):
|
||||||
|
"""Stream alias 'front_door_main' is owned by front_door; limited_user (who
|
||||||
|
is allowed front_door) should be permitted."""
|
||||||
|
app = self._make_app(_MULTI_CAMERA_CONFIG)
|
||||||
|
# front_door_main is the alias defined in live.streams for front_door
|
||||||
|
resp = self._get_stream(app, "front_door_main", role="limited_user")
|
||||||
|
assert resp.status_code not in (401, 403), (
|
||||||
|
f"limited_user should be allowed on alias front_door_main; "
|
||||||
|
f"got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
|
||||||
|
"""limited_user cannot access a stream alias that belongs to a camera they
|
||||||
|
are not allowed to see."""
|
||||||
|
# Give back_door a stream alias and restrict limited_user to front_door only
|
||||||
|
config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"auth": {
|
||||||
|
"roles": {
|
||||||
|
"limited_user": ["front_door"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"front_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
},
|
||||||
|
"back_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
"live": {"streams": {"default": "back_door_main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app = self._make_app(config)
|
||||||
|
resp = self._get_stream(app, "back_door_main", role="limited_user")
|
||||||
|
assert resp.status_code == 403, (
|
||||||
|
f"limited_user should be denied on alias back_door_main; "
|
||||||
|
f"got {resp.status_code}"
|
||||||
|
)
|
||||||
|
|||||||
105
frigate/test/test_env.py
Normal file
105
frigate/test/test_env.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
"""Tests for environment variable handling."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from frigate.config.env import (
|
||||||
|
FRIGATE_ENV_VARS,
|
||||||
|
validate_env_string,
|
||||||
|
validate_env_vars,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvString(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._original_env_vars = dict(FRIGATE_ENV_VARS)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
FRIGATE_ENV_VARS.clear()
|
||||||
|
FRIGATE_ENV_VARS.update(self._original_env_vars)
|
||||||
|
|
||||||
|
def test_substitution(self):
|
||||||
|
"""EnvString substitutes FRIGATE_ env vars."""
|
||||||
|
FRIGATE_ENV_VARS["FRIGATE_TEST_HOST"] = "192.168.1.100"
|
||||||
|
result = validate_env_string("{FRIGATE_TEST_HOST}")
|
||||||
|
self.assertEqual(result, "192.168.1.100")
|
||||||
|
|
||||||
|
def test_substitution_in_url(self):
|
||||||
|
"""EnvString substitutes vars embedded in a URL."""
|
||||||
|
FRIGATE_ENV_VARS["FRIGATE_CAM_USER"] = "admin"
|
||||||
|
FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"] = "secret"
|
||||||
|
result = validate_env_string(
|
||||||
|
"rtsp://{FRIGATE_CAM_USER}:{FRIGATE_CAM_PASS}@10.0.0.1/stream"
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "rtsp://admin:secret@10.0.0.1/stream")
|
||||||
|
|
||||||
|
def test_no_placeholder(self):
|
||||||
|
"""Plain strings pass through unchanged."""
|
||||||
|
result = validate_env_string("192.168.1.1")
|
||||||
|
self.assertEqual(result, "192.168.1.1")
|
||||||
|
|
||||||
|
def test_unknown_var_raises(self):
|
||||||
|
"""Referencing an unknown var raises KeyError."""
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
validate_env_string("{FRIGATE_NONEXISTENT_VAR}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvVars(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._original_env_vars = dict(FRIGATE_ENV_VARS)
|
||||||
|
self._original_environ = os.environ.copy()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
FRIGATE_ENV_VARS.clear()
|
||||||
|
FRIGATE_ENV_VARS.update(self._original_env_vars)
|
||||||
|
# Clean up any env vars we set
|
||||||
|
for key in list(os.environ.keys()):
|
||||||
|
if key not in self._original_environ:
|
||||||
|
del os.environ[key]
|
||||||
|
|
||||||
|
def _make_context(self, install: bool):
|
||||||
|
"""Create a mock ValidationInfo with the given install flag."""
|
||||||
|
|
||||||
|
class MockContext:
|
||||||
|
def __init__(self, ctx):
|
||||||
|
self.context = ctx
|
||||||
|
|
||||||
|
mock = MockContext({"install": install})
|
||||||
|
return mock
|
||||||
|
|
||||||
|
def test_install_sets_os_environ(self):
|
||||||
|
"""validate_env_vars with install=True sets os.environ."""
|
||||||
|
ctx = self._make_context(install=True)
|
||||||
|
validate_env_vars({"MY_CUSTOM_VAR": "value123"}, ctx)
|
||||||
|
self.assertEqual(os.environ.get("MY_CUSTOM_VAR"), "value123")
|
||||||
|
|
||||||
|
def test_install_updates_frigate_env_vars(self):
|
||||||
|
"""validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys."""
|
||||||
|
ctx = self._make_context(install=True)
|
||||||
|
validate_env_vars({"FRIGATE_MQTT_PASS": "secret"}, ctx)
|
||||||
|
self.assertEqual(FRIGATE_ENV_VARS["FRIGATE_MQTT_PASS"], "secret")
|
||||||
|
|
||||||
|
def test_install_skips_non_frigate_in_env_vars_dict(self):
|
||||||
|
"""Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS."""
|
||||||
|
ctx = self._make_context(install=True)
|
||||||
|
validate_env_vars({"OTHER_VAR": "value"}, ctx)
|
||||||
|
self.assertEqual(os.environ.get("OTHER_VAR"), "value")
|
||||||
|
self.assertNotIn("OTHER_VAR", FRIGATE_ENV_VARS)
|
||||||
|
|
||||||
|
def test_no_install_does_not_set(self):
|
||||||
|
"""validate_env_vars without install=True does not modify state."""
|
||||||
|
ctx = self._make_context(install=False)
|
||||||
|
validate_env_vars({"FRIGATE_SKIP": "nope"}, ctx)
|
||||||
|
self.assertNotIn("FRIGATE_SKIP", FRIGATE_ENV_VARS)
|
||||||
|
self.assertNotIn("FRIGATE_SKIP", os.environ)
|
||||||
|
|
||||||
|
def test_env_vars_available_for_env_string(self):
|
||||||
|
"""Vars set via validate_env_vars are usable in validate_env_string."""
|
||||||
|
ctx = self._make_context(install=True)
|
||||||
|
validate_env_vars({"FRIGATE_BROKER": "mqtt.local"}, ctx)
|
||||||
|
result = validate_env_string("{FRIGATE_BROKER}")
|
||||||
|
self.assertEqual(result, "mqtt.local")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -55,6 +55,14 @@ DYNAMIC_OBJECT_THRESHOLDS = StationaryThresholds(
|
|||||||
motion_classifier_enabled=True,
|
motion_classifier_enabled=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Thresholds for objects that are not expected to be stationary
|
||||||
|
NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds(
|
||||||
|
objects=["license_plate"],
|
||||||
|
known_active_iou=0.9,
|
||||||
|
stationary_check_iou=0.9,
|
||||||
|
max_stationary_history=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_stationary_threshold(label: str) -> StationaryThresholds:
|
def get_stationary_threshold(label: str) -> StationaryThresholds:
|
||||||
"""Get the stationary thresholds for a given object label."""
|
"""Get the stationary thresholds for a given object label."""
|
||||||
@ -65,6 +73,9 @@ def get_stationary_threshold(label: str) -> StationaryThresholds:
|
|||||||
if label in DYNAMIC_OBJECT_THRESHOLDS.objects:
|
if label in DYNAMIC_OBJECT_THRESHOLDS.objects:
|
||||||
return DYNAMIC_OBJECT_THRESHOLDS
|
return DYNAMIC_OBJECT_THRESHOLDS
|
||||||
|
|
||||||
|
if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects:
|
||||||
|
return NON_STATIONARY_OBJECT_THRESHOLDS
|
||||||
|
|
||||||
return StationaryThresholds()
|
return StationaryThresholds()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,20 @@ import datetime
|
|||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess as sp
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from peewee import DatabaseError, chunked
|
from peewee import DatabaseError, chunked
|
||||||
|
|
||||||
from frigate.const import CLIPS_DIR, EXPORT_DIR, RECORD_DIR, THUMB_DIR
|
from frigate.const import (
|
||||||
|
CLIPS_DIR,
|
||||||
|
DEFAULT_FFMPEG_VERSION,
|
||||||
|
EXPORT_DIR,
|
||||||
|
RECORD_DIR,
|
||||||
|
THUMB_DIR,
|
||||||
|
)
|
||||||
from frigate.models import (
|
from frigate.models import (
|
||||||
Event,
|
Event,
|
||||||
Export,
|
Export,
|
||||||
@ -26,6 +33,12 @@ logger = logging.getLogger(__name__)
|
|||||||
# Safety threshold - abort if more than 50% of files would be deleted
|
# Safety threshold - abort if more than 50% of files would be deleted
|
||||||
SAFETY_THRESHOLD = 0.5
|
SAFETY_THRESHOLD = 0.5
|
||||||
|
|
||||||
|
FFPROBE_PATH = (
|
||||||
|
f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe"
|
||||||
|
if DEFAULT_FFMPEG_VERSION
|
||||||
|
else "ffprobe"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SyncResult:
|
class SyncResult:
|
||||||
@ -808,3 +821,53 @@ def sync_all_media(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def get_keyframe_before(path: str, offset_ms: int) -> int | None:
|
||||||
|
"""Get the timestamp (ms) of the last keyframe at or before offset_ms.
|
||||||
|
|
||||||
|
Uses ffprobe packet index to read keyframe positions from the mp4 file.
|
||||||
|
Returns None if ffprobe fails or no keyframe is found before the offset.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = sp.run(
|
||||||
|
[
|
||||||
|
FFPROBE_PATH,
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"packet=pts_time,flags",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
path,
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except (sp.TimeoutExpired, FileNotFoundError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset_s = offset_ms / 1000.0
|
||||||
|
best_ms = None
|
||||||
|
for line in result.stdout.decode().strip().splitlines():
|
||||||
|
parts = line.strip().split(",")
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
ts_str, flags = parts
|
||||||
|
if "K" not in flags:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ts = float(ts_str)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if ts <= offset_s:
|
||||||
|
best_ms = int(ts * 1000)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return best_ms
|
||||||
|
|||||||
@ -515,7 +515,7 @@ class CameraWatchdog(threading.Thread):
|
|||||||
|
|
||||||
for role in p["roles"]:
|
for role in p["roles"]:
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
f"{self.config.name}/status/{role}", "offline"
|
f"{self.config.name}/status/{role.value}", "offline"
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@ -528,7 +528,7 @@ class CameraWatchdog(threading.Thread):
|
|||||||
|
|
||||||
for role in p["roles"]:
|
for role in p["roles"]:
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
f"{self.config.name}/status/{role}", "offline"
|
f"{self.config.name}/status/{role.value}", "offline"
|
||||||
)
|
)
|
||||||
|
|
||||||
p["logpipe"].dump()
|
p["logpipe"].dump()
|
||||||
|
|||||||
@ -213,6 +213,7 @@ export function AnimatedEventCard({
|
|||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
disableRemotePlayback
|
disableRemotePlayback
|
||||||
|
disablePictureInPicture
|
||||||
loop
|
loop
|
||||||
onTimeUpdate={() => {
|
onTimeUpdate={() => {
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
|
|||||||
@ -77,6 +77,7 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider";
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
|
||||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||||
|
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state";
|
import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state";
|
||||||
|
|
||||||
@ -677,7 +678,7 @@ export function CameraGroupEdit({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const allowedCameras = useAllowedCameras();
|
const allowedCameras = useAllowedCameras();
|
||||||
const isAdmin = useIsAdmin();
|
const hasFullCameraAccess = useHasFullCameraAccess();
|
||||||
|
|
||||||
const [openCamera, setOpenCamera] = useState<string | null>();
|
const [openCamera, setOpenCamera] = useState<string | null>();
|
||||||
|
|
||||||
@ -866,8 +867,7 @@ export function CameraGroupEdit({
|
|||||||
<FormDescription>{t("group.cameras.desc")}</FormDescription>
|
<FormDescription>{t("group.cameras.desc")}</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{[
|
{[
|
||||||
...(birdseyeConfig?.enabled &&
|
...(birdseyeConfig?.enabled && hasFullCameraAccess
|
||||||
(isAdmin || "birdseye" in allowedCameras)
|
|
||||||
? ["birdseye"]
|
? ["birdseye"]
|
||||||
: []),
|
: []),
|
||||||
...Object.keys(config?.cameras ?? {})
|
...Object.keys(config?.cameras ?? {})
|
||||||
|
|||||||
@ -126,19 +126,21 @@ export default function AccountSettings({ className }: AccountSettingsProps) {
|
|||||||
|
|
||||||
<DropdownMenuSeparator className={isDesktop ? "my-2" : "my-2"} />
|
<DropdownMenuSeparator className={isDesktop ? "my-2" : "my-2"} />
|
||||||
|
|
||||||
{profile?.username && profile.username !== "anonymous" && (
|
{config?.auth?.enabled !== false &&
|
||||||
<MenuItem
|
profile?.username &&
|
||||||
className={cn(
|
profile.username !== "anonymous" && (
|
||||||
"flex w-full items-center gap-2",
|
<MenuItem
|
||||||
isDesktop ? "cursor-pointer" : "p-2 text-sm",
|
className={cn(
|
||||||
)}
|
"flex w-full items-center gap-2",
|
||||||
aria-label={t("menu.user.setPassword", { ns: "common" })}
|
isDesktop ? "cursor-pointer" : "p-2 text-sm",
|
||||||
onClick={() => setPasswordDialogOpen(true)}
|
)}
|
||||||
>
|
aria-label={t("menu.user.setPassword", { ns: "common" })}
|
||||||
<LuSquarePen className="mr-2 size-4" />
|
onClick={() => setPasswordDialogOpen(true)}
|
||||||
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
|
>
|
||||||
</MenuItem>
|
<LuSquarePen className="mr-2 size-4" />
|
||||||
)}
|
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -266,20 +266,24 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
<DropdownMenuSeparator
|
<DropdownMenuSeparator
|
||||||
className={isDesktop ? "mt-3" : "mt-1"}
|
className={isDesktop ? "mt-3" : "mt-1"}
|
||||||
/>
|
/>
|
||||||
{profile?.username && profile.username !== "anonymous" && (
|
{config?.auth?.enabled !== false &&
|
||||||
<MenuItem
|
profile?.username &&
|
||||||
className={
|
profile.username !== "anonymous" && (
|
||||||
isDesktop
|
<MenuItem
|
||||||
? "cursor-pointer"
|
className={
|
||||||
: "flex items-center p-2 text-sm"
|
isDesktop
|
||||||
}
|
? "cursor-pointer"
|
||||||
aria-label={t("menu.user.setPassword", { ns: "common" })}
|
: "flex items-center p-2 text-sm"
|
||||||
onClick={() => setPasswordDialogOpen(true)}
|
}
|
||||||
>
|
aria-label={t("menu.user.setPassword", { ns: "common" })}
|
||||||
<LuSquarePen className="mr-2 size-4" />
|
onClick={() => setPasswordDialogOpen(true)}
|
||||||
<span>{t("menu.user.setPassword", { ns: "common" })}</span>
|
>
|
||||||
</MenuItem>
|
<LuSquarePen className="mr-2 size-4" />
|
||||||
)}
|
<span>
|
||||||
|
{t("menu.user.setPassword", { ns: "common" })}
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={
|
className={
|
||||||
isDesktop
|
isDesktop
|
||||||
|
|||||||
@ -125,17 +125,23 @@ export default function ClassificationSelectionDialog({
|
|||||||
isMobile && "gap-2 pb-4",
|
isMobile && "gap-2 pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{classes.sort().map((category) => (
|
{classes
|
||||||
<SelectorItem
|
.sort((a, b) => {
|
||||||
key={category}
|
if (a === "none") return 1;
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
if (b === "none") return -1;
|
||||||
onClick={() => onCategorizeImage(category)}
|
return a.localeCompare(b);
|
||||||
>
|
})
|
||||||
{category === "none"
|
.map((category) => (
|
||||||
? t("details.none")
|
<SelectorItem
|
||||||
: category.replaceAll("_", " ")}
|
key={category}
|
||||||
</SelectorItem>
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
))}
|
onClick={() => onCategorizeImage(category)}
|
||||||
|
>
|
||||||
|
{category === "none"
|
||||||
|
? t("details.none")
|
||||||
|
: category.replaceAll("_", " ")}
|
||||||
|
</SelectorItem>
|
||||||
|
))}
|
||||||
<Separator />
|
<Separator />
|
||||||
<SelectorItem
|
<SelectorItem
|
||||||
className="flex cursor-pointer gap-2 smart-capitalize"
|
className="flex cursor-pointer gap-2 smart-capitalize"
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator
|
|||||||
import ObjectTrackOverlay from "../ObjectTrackOverlay";
|
import ObjectTrackOverlay from "../ObjectTrackOverlay";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
|
import { VodManifest } from "@/types/playback";
|
||||||
|
|
||||||
type TrackingDetailsProps = {
|
type TrackingDetailsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -133,19 +134,64 @@ export function TrackingDetails({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Fetch the VOD manifest JSON to get the actual clipFrom after keyframe
|
||||||
|
// snapping. The backend may snap clipFrom backwards to a keyframe, making
|
||||||
|
// the video start earlier than the requested time.
|
||||||
|
const vodManifestUrl = useMemo(() => {
|
||||||
|
if (!event.camera) return null;
|
||||||
|
const startTime =
|
||||||
|
event.start_time + annotationOffset / 1000 - REVIEW_PADDING;
|
||||||
|
const endTime =
|
||||||
|
(event.end_time ?? Date.now() / 1000) +
|
||||||
|
annotationOffset / 1000 +
|
||||||
|
REVIEW_PADDING;
|
||||||
|
return `vod/clip/${event.camera}/start/${startTime}/end/${endTime}`;
|
||||||
|
}, [event, annotationOffset]);
|
||||||
|
|
||||||
|
const { data: vodManifest } = useSWR<VodManifest>(vodManifestUrl, null, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive the actual video start time from the VOD manifest's first clip.
|
||||||
|
// Without this correction the timeline-to-player-time mapping is off by
|
||||||
|
// the keyframe preroll amount.
|
||||||
|
const actualVideoStart = useMemo(() => {
|
||||||
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
|
||||||
|
if (!vodManifest?.sequences?.[0]?.clips?.[0] || !recordings?.length) {
|
||||||
|
return videoStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstClip = vodManifest.sequences[0].clips[0];
|
||||||
|
|
||||||
|
// Guard: clipFrom is only expected when the first recording starts before
|
||||||
|
// the requested start. If this doesn't hold, fall back.
|
||||||
|
if (recordings[0].start_time >= videoStartTime) {
|
||||||
|
return recordings[0].start_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstClip.clipFrom !== undefined) {
|
||||||
|
// clipFrom is in milliseconds from the start of the first recording
|
||||||
|
return recordings[0].start_time + firstClip.clipFrom / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipFrom absent means the full recording is included (keyframe probe failed)
|
||||||
|
return recordings[0].start_time;
|
||||||
|
}, [vodManifest, recordings, eventStartRecord]);
|
||||||
|
|
||||||
// Convert a timeline timestamp to actual video player time, accounting for
|
// Convert a timeline timestamp to actual video player time, accounting for
|
||||||
// motion-only recording gaps. Uses the same algorithm as DynamicVideoController.
|
// motion-only recording gaps. Uses the same algorithm as DynamicVideoController.
|
||||||
const timestampToVideoTime = useCallback(
|
const timestampToVideoTime = useCallback(
|
||||||
(timestamp: number): number => {
|
(timestamp: number): number => {
|
||||||
if (!recordings || recordings.length === 0) {
|
if (!recordings || recordings.length === 0) {
|
||||||
// Fallback to simple calculation if no recordings data
|
// Fallback to simple calculation if no recordings data
|
||||||
return timestamp - (eventStartRecord - REVIEW_PADDING);
|
return timestamp - actualVideoStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
// If timestamp is before actual video start, return 0
|
||||||
|
if (timestamp < actualVideoStart) return 0;
|
||||||
// If timestamp is before video start, return 0
|
|
||||||
if (timestamp < videoStartTime) return 0;
|
|
||||||
|
|
||||||
// Check if timestamp is before the first recording or after the last
|
// Check if timestamp is before the first recording or after the last
|
||||||
if (
|
if (
|
||||||
@ -159,10 +205,10 @@ export function TrackingDetails({
|
|||||||
// Calculate the inpoint offset - the HLS video may start partway through the first segment
|
// Calculate the inpoint offset - the HLS video may start partway through the first segment
|
||||||
let inpointOffset = 0;
|
let inpointOffset = 0;
|
||||||
if (
|
if (
|
||||||
videoStartTime > recordings[0].start_time &&
|
actualVideoStart > recordings[0].start_time &&
|
||||||
videoStartTime < recordings[0].end_time
|
actualVideoStart < recordings[0].end_time
|
||||||
) {
|
) {
|
||||||
inpointOffset = videoStartTime - recordings[0].start_time;
|
inpointOffset = actualVideoStart - recordings[0].start_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
let seekSeconds = 0;
|
let seekSeconds = 0;
|
||||||
@ -180,7 +226,7 @@ export function TrackingDetails({
|
|||||||
if (segment === recordings[0]) {
|
if (segment === recordings[0]) {
|
||||||
// For the first segment, account for the inpoint offset
|
// For the first segment, account for the inpoint offset
|
||||||
seekSeconds +=
|
seekSeconds +=
|
||||||
timestamp - Math.max(segment.start_time, videoStartTime);
|
timestamp - Math.max(segment.start_time, actualVideoStart);
|
||||||
} else {
|
} else {
|
||||||
seekSeconds += timestamp - segment.start_time;
|
seekSeconds += timestamp - segment.start_time;
|
||||||
}
|
}
|
||||||
@ -190,7 +236,7 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
return seekSeconds;
|
return seekSeconds;
|
||||||
},
|
},
|
||||||
[recordings, eventStartRecord],
|
[recordings, actualVideoStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert video player time back to timeline timestamp, accounting for
|
// Convert video player time back to timeline timestamp, accounting for
|
||||||
@ -199,19 +245,16 @@ export function TrackingDetails({
|
|||||||
(playerTime: number): number => {
|
(playerTime: number): number => {
|
||||||
if (!recordings || recordings.length === 0) {
|
if (!recordings || recordings.length === 0) {
|
||||||
// Fallback to simple calculation if no recordings data
|
// Fallback to simple calculation if no recordings data
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
return playerTime + actualVideoStart;
|
||||||
return playerTime + videoStartTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
|
||||||
|
|
||||||
// Calculate the inpoint offset - the video may start partway through the first segment
|
// Calculate the inpoint offset - the video may start partway through the first segment
|
||||||
let inpointOffset = 0;
|
let inpointOffset = 0;
|
||||||
if (
|
if (
|
||||||
videoStartTime > recordings[0].start_time &&
|
actualVideoStart > recordings[0].start_time &&
|
||||||
videoStartTime < recordings[0].end_time
|
actualVideoStart < recordings[0].end_time
|
||||||
) {
|
) {
|
||||||
inpointOffset = videoStartTime - recordings[0].start_time;
|
inpointOffset = actualVideoStart - recordings[0].start_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = 0;
|
let timestamp = 0;
|
||||||
@ -228,7 +271,7 @@ export function TrackingDetails({
|
|||||||
if (segment === recordings[0]) {
|
if (segment === recordings[0]) {
|
||||||
// For the first segment, add the inpoint offset
|
// For the first segment, add the inpoint offset
|
||||||
timestamp =
|
timestamp =
|
||||||
Math.max(segment.start_time, videoStartTime) +
|
Math.max(segment.start_time, actualVideoStart) +
|
||||||
(playerTime - totalTime);
|
(playerTime - totalTime);
|
||||||
} else {
|
} else {
|
||||||
timestamp = segment.start_time + (playerTime - totalTime);
|
timestamp = segment.start_time + (playerTime - totalTime);
|
||||||
@ -241,7 +284,7 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
return timestamp;
|
return timestamp;
|
||||||
},
|
},
|
||||||
[recordings, eventStartRecord],
|
[recordings, actualVideoStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
eventSequence?.map((event) => {
|
eventSequence?.map((event) => {
|
||||||
@ -1080,7 +1123,7 @@ function LifecycleIconRow({
|
|||||||
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
|
<div className="ml-3 flex-shrink-0 px-1 text-right text-xs text-primary-variant">
|
||||||
<div className="flex flex-row items-center gap-3">
|
<div className="flex flex-row items-center gap-3">
|
||||||
<div className="whitespace-nowrap">{formattedEventTimestamp}</div>
|
<div className="whitespace-nowrap">{formattedEventTimestamp}</div>
|
||||||
{isAdmin && config?.plus?.enabled && item.data.box && (
|
{isAdmin && (config?.plus?.enabled || item.data.box) && (
|
||||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<div className="rounded p-1 pr-2" role="button">
|
<div className="rounded p-1 pr-2" role="button">
|
||||||
|
|||||||
@ -357,7 +357,7 @@ export default function HlsVideoPlayer({
|
|||||||
{transformedOverlay}
|
{transformedOverlay}
|
||||||
{isDetailMode &&
|
{isDetailMode &&
|
||||||
camera &&
|
camera &&
|
||||||
currentTime &&
|
currentTime != null &&
|
||||||
loadedMetadata &&
|
loadedMetadata &&
|
||||||
videoDimensions.width > 0 &&
|
videoDimensions.width > 0 &&
|
||||||
videoDimensions.height > 0 && (
|
videoDimensions.height > 0 && (
|
||||||
|
|||||||
@ -20,7 +20,10 @@ import type {
|
|||||||
CameraConfigData,
|
CameraConfigData,
|
||||||
ConfigSetBody,
|
ConfigSetBody,
|
||||||
} from "@/types/cameraWizard";
|
} from "@/types/cameraWizard";
|
||||||
import { processCameraName } from "@/utils/cameraUtil";
|
import {
|
||||||
|
processCameraName,
|
||||||
|
calculateDetectDimensions,
|
||||||
|
} from "@/utils/cameraUtil";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type WizardState = {
|
type WizardState = {
|
||||||
@ -203,6 +206,25 @@ export default function CameraWizardDialog({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate detect dimensions from the detect stream's probed resolution
|
||||||
|
const detectStream = wizardData.streams.find((stream) =>
|
||||||
|
stream.roles.includes("detect"),
|
||||||
|
);
|
||||||
|
if (detectStream?.testResult?.resolution) {
|
||||||
|
const [streamWidth, streamHeight] = detectStream.testResult.resolution
|
||||||
|
.split("x")
|
||||||
|
.map(Number);
|
||||||
|
if (streamWidth > 0 && streamHeight > 0) {
|
||||||
|
const detectDimensions = calculateDetectDimensions(
|
||||||
|
streamWidth,
|
||||||
|
streamHeight,
|
||||||
|
);
|
||||||
|
if (detectDimensions) {
|
||||||
|
configData.cameras[finalCameraName].detect = detectDimensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add live.streams configuration for go2rtc streams
|
// Add live.streams configuration for go2rtc streams
|
||||||
if (wizardData.streams && wizardData.streams.length > 0) {
|
if (wizardData.streams && wizardData.streams.length > 0) {
|
||||||
configData.cameras[finalCameraName].live = {
|
configData.cameras[finalCameraName].live = {
|
||||||
|
|||||||
@ -18,18 +18,25 @@ export default function useCameraLiveMode(
|
|||||||
|
|
||||||
const streamNames = new Set<string>();
|
const streamNames = new Set<string>();
|
||||||
cameras.forEach((camera) => {
|
cameras.forEach((camera) => {
|
||||||
const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes(
|
if (activeStreams && activeStreams[camera.name]) {
|
||||||
Object.values(camera.live.streams)[0],
|
const selectedStreamName = activeStreams[camera.name];
|
||||||
);
|
const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes(
|
||||||
|
selectedStreamName,
|
||||||
|
);
|
||||||
|
|
||||||
if (isRestreamed) {
|
if (isRestreamed) {
|
||||||
if (activeStreams && activeStreams[camera.name]) {
|
streamNames.add(selectedStreamName);
|
||||||
streamNames.add(activeStreams[camera.name]);
|
|
||||||
} else {
|
|
||||||
Object.values(camera.live.streams).forEach((streamName) => {
|
|
||||||
streamNames.add(streamName);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Object.values(camera.live.streams).forEach((streamName) => {
|
||||||
|
const isRestreamed = Object.keys(
|
||||||
|
config.go2rtc.streams || {},
|
||||||
|
).includes(streamName);
|
||||||
|
|
||||||
|
if (isRestreamed) {
|
||||||
|
streamNames.add(streamName);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,11 +73,11 @@ export default function useCameraLiveMode(
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
cameras.forEach((camera) => {
|
cameras.forEach((camera) => {
|
||||||
|
const selectedStreamName =
|
||||||
|
activeStreams?.[camera.name] ?? Object.values(camera.live.streams)[0];
|
||||||
const isRestreamed =
|
const isRestreamed =
|
||||||
config &&
|
config &&
|
||||||
Object.keys(config.go2rtc.streams || {}).includes(
|
Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
|
||||||
Object.values(camera.live.streams)[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
newIsRestreamedStates[camera.name] = isRestreamed ?? false;
|
newIsRestreamedStates[camera.name] = isRestreamed ?? false;
|
||||||
|
|
||||||
@ -101,14 +108,21 @@ export default function useCameraLiveMode(
|
|||||||
setPreferredLiveModes(newPreferredLiveModes);
|
setPreferredLiveModes(newPreferredLiveModes);
|
||||||
setIsRestreamedStates(newIsRestreamedStates);
|
setIsRestreamedStates(newIsRestreamedStates);
|
||||||
setSupportsAudioOutputStates(newSupportsAudioOutputStates);
|
setSupportsAudioOutputStates(newSupportsAudioOutputStates);
|
||||||
}, [cameras, config, windowVisible, streamMetadata]);
|
}, [activeStreams, cameras, config, windowVisible, streamMetadata]);
|
||||||
|
|
||||||
const resetPreferredLiveMode = useCallback(
|
const resetPreferredLiveMode = useCallback(
|
||||||
(cameraName: string) => {
|
(cameraName: string) => {
|
||||||
const mseSupported =
|
const mseSupported =
|
||||||
"MediaSource" in window || "ManagedMediaSource" in window;
|
"MediaSource" in window || "ManagedMediaSource" in window;
|
||||||
|
const cameraConfig = cameras.find((camera) => camera.name === cameraName);
|
||||||
|
const selectedStreamName =
|
||||||
|
activeStreams?.[cameraName] ??
|
||||||
|
(cameraConfig
|
||||||
|
? Object.values(cameraConfig.live.streams)[0]
|
||||||
|
: cameraName);
|
||||||
const isRestreamed =
|
const isRestreamed =
|
||||||
config && Object.keys(config.go2rtc.streams || {}).includes(cameraName);
|
config &&
|
||||||
|
Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
|
||||||
|
|
||||||
setPreferredLiveModes((prevModes) => {
|
setPreferredLiveModes((prevModes) => {
|
||||||
const newModes = { ...prevModes };
|
const newModes = { ...prevModes };
|
||||||
@ -122,7 +136,7 @@ export default function useCameraLiveMode(
|
|||||||
return newModes;
|
return newModes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[config],
|
[activeStreams, cameras, config],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
26
web/src/hooks/use-has-full-camera-access.ts
Normal file
26
web/src/hooks/use-has-full-camera-access.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the current user has access to all cameras.
|
||||||
|
* This is used to determine birdseye access — users who can see
|
||||||
|
* all cameras should also be able to see the birdseye view.
|
||||||
|
*/
|
||||||
|
export function useHasFullCameraAccess() {
|
||||||
|
const allowedCameras = useAllowedCameras();
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config?.cameras) return false;
|
||||||
|
|
||||||
|
const enabledCameraNames = Object.entries(config.cameras)
|
||||||
|
.filter(([, cam]) => cam.enabled_in_config)
|
||||||
|
.map(([name]) => name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
enabledCameraNames.length > 0 &&
|
||||||
|
enabledCameraNames.every((name) => allowedCameras.includes(name))
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -598,18 +598,18 @@ function LibrarySelector({
|
|||||||
{Object.values(faces).map((face) => (
|
{Object.values(faces).map((face) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={face}
|
key={face}
|
||||||
className="group flex items-center justify-between"
|
className="group flex items-center justify-between p-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex-grow cursor-pointer"
|
className="flex-grow cursor-pointer"
|
||||||
onClick={() => setPageToggle(face)}
|
onClick={() => setPageToggle(face)}
|
||||||
>
|
>
|
||||||
{face}
|
{face}
|
||||||
<span className="ml-2 text-muted-foreground">
|
<span className="ml-2 px-2 py-1.5 text-muted-foreground">
|
||||||
({faceData?.[face].length})
|
({faceData?.[face].length})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-0.5">
|
<div className="flex gap-0.5 px-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -11,12 +11,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
|
||||||
|
|
||||||
function Live() {
|
function Live() {
|
||||||
const { t } = useTranslation(["views/live"]);
|
const { t } = useTranslation(["views/live"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const isAdmin = useIsAdmin();
|
const hasFullCameraAccess = useHasFullCameraAccess();
|
||||||
|
|
||||||
// selection
|
// selection
|
||||||
|
|
||||||
@ -90,8 +90,8 @@ function Live() {
|
|||||||
const allowedCameras = useAllowedCameras();
|
const allowedCameras = useAllowedCameras();
|
||||||
|
|
||||||
const includesBirdseye = useMemo(() => {
|
const includesBirdseye = useMemo(() => {
|
||||||
// Restricted users should never have access to birdseye
|
// Users without access to all cameras should not have access to birdseye
|
||||||
if (!isAdmin) {
|
if (!hasFullCameraAccess) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ function Live() {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [config, cameraGroup, isAdmin]);
|
}, [config, cameraGroup, hasFullCameraAccess]);
|
||||||
|
|
||||||
const cameras = useMemo(() => {
|
const cameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -151,7 +151,9 @@ function Live() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full" ref={mainRef}>
|
<div className="size-full" ref={mainRef}>
|
||||||
{selectedCameraName === "birdseye" ? (
|
{selectedCameraName === "birdseye" &&
|
||||||
|
hasFullCameraAccess &&
|
||||||
|
config?.birdseye?.enabled ? (
|
||||||
<LiveBirdseyeView
|
<LiveBirdseyeView
|
||||||
supportsFullscreen={supportsFullScreen}
|
supportsFullscreen={supportsFullScreen}
|
||||||
fullscreen={fullscreen}
|
fullscreen={fullscreen}
|
||||||
|
|||||||
@ -162,6 +162,10 @@ export type CameraConfigData = {
|
|||||||
input_args?: string;
|
input_args?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
detect?: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
live?: {
|
live?: {
|
||||||
streams: Record<string, string>;
|
streams: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -401,6 +401,7 @@ export interface FrigateConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
|
enabled: boolean;
|
||||||
roles: {
|
roles: {
|
||||||
[roleName: string]: string[];
|
[roleName: string]: string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,3 +11,7 @@ export type PreviewPlayback = {
|
|||||||
preview: Preview | undefined;
|
preview: Preview | undefined;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VodManifest = {
|
||||||
|
sequences: { clips: { clipFrom?: number }[] }[];
|
||||||
|
};
|
||||||
|
|||||||
@ -115,6 +115,51 @@ export type CameraAudioFeatures = {
|
|||||||
* @param requireSecureContext - If true, two-way audio requires secure context (default: true)
|
* @param requireSecureContext - If true, two-way audio requires secure context (default: true)
|
||||||
* @returns CameraAudioFeatures object with detected capabilities
|
* @returns CameraAudioFeatures object with detected capabilities
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Calculates optimal detect dimensions from stream resolution.
|
||||||
|
*
|
||||||
|
* Scales dimensions to an efficient size for object detection while
|
||||||
|
* preserving the stream's aspect ratio. Does not upscale.
|
||||||
|
*
|
||||||
|
* @param streamWidth - Native stream width in pixels
|
||||||
|
* @param streamHeight - Native stream height in pixels
|
||||||
|
* @returns Detect dimensions with even values, or null if inputs are invalid
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Target size for the smaller dimension (width or height) for detect streams
|
||||||
|
export const DETECT_TARGET_PX = 720;
|
||||||
|
|
||||||
|
export function calculateDetectDimensions(
|
||||||
|
streamWidth: number,
|
||||||
|
streamHeight: number,
|
||||||
|
): { width: number; height: number } | null {
|
||||||
|
if (
|
||||||
|
!Number.isFinite(streamWidth) ||
|
||||||
|
!Number.isFinite(streamHeight) ||
|
||||||
|
streamWidth <= 0 ||
|
||||||
|
streamHeight <= 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smallerDim = Math.min(streamWidth, streamHeight);
|
||||||
|
const target = Math.min(DETECT_TARGET_PX, smallerDim);
|
||||||
|
const scale = target / smallerDim;
|
||||||
|
|
||||||
|
let width = Math.round(streamWidth * scale);
|
||||||
|
let height = Math.round(streamHeight * scale);
|
||||||
|
|
||||||
|
// Round down to even numbers (required for video processing)
|
||||||
|
width = width - (width % 2);
|
||||||
|
height = height - (height % 2);
|
||||||
|
|
||||||
|
if (width < 2 || height < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
export function detectCameraAudioFeatures(
|
export function detectCameraAudioFeatures(
|
||||||
metadata: LiveStreamMetadata | null | undefined,
|
metadata: LiveStreamMetadata | null | undefined,
|
||||||
requireSecureContext: boolean = true,
|
requireSecureContext: boolean = true,
|
||||||
|
|||||||
@ -700,66 +700,72 @@ function LibrarySelector({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{Object.keys(dataset).map((id) => (
|
{Object.keys(dataset)
|
||||||
<DropdownMenuItem
|
.sort((a, b) => {
|
||||||
key={id}
|
if (a === "none") return 1;
|
||||||
className="group flex items-center justify-between"
|
if (b === "none") return -1;
|
||||||
>
|
return a.localeCompare(b);
|
||||||
<div
|
})
|
||||||
className="flex-grow cursor-pointer capitalize"
|
.map((id) => (
|
||||||
onClick={() => setPageToggle(id)}
|
<DropdownMenuItem
|
||||||
|
key={id}
|
||||||
|
className="group flex items-center justify-between p-0"
|
||||||
>
|
>
|
||||||
{id === "none" ? t("details.none") : id.replaceAll("_", " ")}
|
<div
|
||||||
<span className="ml-2 text-muted-foreground">
|
className="flex-grow cursor-pointer px-2 py-1.5 capitalize"
|
||||||
({dataset?.[id].length})
|
onClick={() => setPageToggle(id)}
|
||||||
</span>
|
>
|
||||||
</div>
|
{id === "none" ? t("details.none") : id.replaceAll("_", " ")}
|
||||||
{id != "none" && (
|
<span className="ml-2 text-muted-foreground">
|
||||||
<div className="flex gap-0.5">
|
({dataset?.[id].length})
|
||||||
<Tooltip>
|
</span>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setRenameClass(id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuPencil className="size-4 text-primary" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPortal>
|
|
||||||
<TooltipContent>
|
|
||||||
{t("button.renameCategory")}
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPortal>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setConfirmDelete(id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LuTrash2 className="size-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPortal>
|
|
||||||
<TooltipContent>
|
|
||||||
{t("button.deleteCategory")}
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPortal>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{id != "none" && (
|
||||||
</DropdownMenuItem>
|
<div className="flex gap-0.5 px-2">
|
||||||
))}
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setRenameClass(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuPencil className="size-4 text-primary" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("button.renameCategory")}
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setConfirmDelete(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuTrash2 className="size-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent>
|
||||||
|
{t("button.deleteCategory")}
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user