mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 21:22:50 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc116ecaf4 | ||
|
|
cf418dd6b6 | ||
|
|
aa42b17911 |
@@ -10,13 +10,15 @@ from ruamel.yaml import YAML
|
||||
|
||||
sys.path.insert(0, "/opt/frigate")
|
||||
from frigate.config.env import substitute_frigate_vars
|
||||
from frigate.const import (
|
||||
BIRDSEYE_PIPE,
|
||||
LIBAVFORMAT_VERSION_MAJOR,
|
||||
)
|
||||
from frigate.const import BIRDSEYE_PIPE
|
||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||
from frigate.util.config import find_config_file, resolve_ffmpeg_path
|
||||
from frigate.util.config import (
|
||||
find_config_file,
|
||||
is_valid_ffmpeg_path,
|
||||
resolve_ffmpeg_path,
|
||||
)
|
||||
from frigate.util.services import (
|
||||
apply_go2rtc_ffmpeg_config,
|
||||
is_go2rtc_arbitrary_exec_allowed,
|
||||
is_restricted_go2rtc_source,
|
||||
)
|
||||
@@ -92,20 +94,17 @@ if go2rtc_config.get("rtsp", {}).get("password") is not None:
|
||||
|
||||
# ensure ffmpeg path is set correctly
|
||||
path = config.get("ffmpeg", {}).get("path", "default")
|
||||
if not is_valid_ffmpeg_path(path):
|
||||
print(
|
||||
"[ERROR] ffmpeg path contains spaces, quotes or shell characters, using the default ffmpeg instead."
|
||||
)
|
||||
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
|
||||
|
||||
if go2rtc_config.get("ffmpeg") is None:
|
||||
go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path}
|
||||
elif go2rtc_config["ffmpeg"].get("bin") is None:
|
||||
go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path
|
||||
|
||||
# need to replace ffmpeg command when using ffmpeg4
|
||||
if LIBAVFORMAT_VERSION_MAJOR < 59:
|
||||
rtsp_args = "-fflags nobuffer -flags low_delay -stimeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}"
|
||||
if go2rtc_config.get("ffmpeg") is None:
|
||||
go2rtc_config["ffmpeg"] = {"rtsp": rtsp_args}
|
||||
elif go2rtc_config["ffmpeg"].get("rtsp") is None:
|
||||
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
|
||||
for key in apply_go2rtc_ffmpeg_config(go2rtc_config, ffmpeg_path):
|
||||
print(
|
||||
f"[ERROR] go2rtc option '{key}' was ignored because it is disabled by default for security. "
|
||||
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec."
|
||||
)
|
||||
|
||||
|
||||
for name in list(go2rtc_config.get("streams", {})):
|
||||
|
||||
@@ -5,6 +5,8 @@ location /auth {
|
||||
## Essential Proxy Configuration
|
||||
internal;
|
||||
proxy_pass $upstream_auth;
|
||||
# $go2rtc_stream_proxy is only set by the go2rtc stream locations
|
||||
uninitialized_variable_warn off;
|
||||
|
||||
## Headers
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ Along with picking a detector for your hardware, you will choose a model's **inp
|
||||
|
||||
:::tip
|
||||
|
||||
The best detection accuracy comes from a model trained on images that look like what Frigate actually sees: security camera footage cropped to regions of interest. You can train or fine-tune your own model on images like this and run it as a custom model (see the per-detector sections below), but [Frigate+](/plus) makes this much easier by handling the training for you on images submitted from your own cameras. For YOLOv9, the `s` (small) variant at `320x320` resolution is a good place to start.
|
||||
The best detection accuracy comes from a model trained on images that look like what Frigate actually sees: security camera footage cropped to regions of interest. You can train or fine-tune your own model on images like this and run it as a custom model (see the per-detector sections below), but [Frigate+](/plus) makes this much easier by handling the training for you on images submitted from your own cameras. For YOLOv9, the `s` (small) variant at `320x320` resolution is a good place to start, and the larger `c` variant is available for more powerful hardware.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ You can view all of your submitted images at [https://plus.frigate.video](https:
|
||||
|
||||
Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically.
|
||||
|
||||
You can either choose the new model from the <NavPath path="Settings > System > Detectors and model" /> pane in the Frigate UI (the **Frigate+ Model** tab), or manually set the model at the root level in your config:
|
||||
You can either choose the new model from the <NavPath path="Settings > System > Detectors and model" /> pane in the Frigate UI (the **Frigate+** tab), or manually set the model at the root level in your config:
|
||||
|
||||
```yaml
|
||||
detectors: ...
|
||||
|
||||
@@ -25,6 +25,12 @@ If you have an active Frigate+ subscription, new uploads will be scanned for the
|
||||
|
||||
Suggestions are converted to labels when saving, so you should remove any errant suggestions. There is already some logic designed to avoid duplicate labels, but you may still occasionally see some duplicate suggestions. You should keep the most accurate bounding box and delete any duplicates so that you have just one label per object remaining.
|
||||
|
||||
## Reviewing annotations with Patches
|
||||
|
||||
The Patches tab on the Images page shows a cropped view of every annotation for a label in a grid. Scanning the crops makes it much easier to spot a mislabeled object than paging through full images one at a time. Pick a label to get started, and optionally narrow the results to a single camera. Clicking a patch opens that image in the editor so you can fix the annotation.
|
||||
|
||||
Patches only include annotations from images you have already verified.
|
||||
|
||||
## False positive labels
|
||||
|
||||
False positives will be shown with a red box and the label will have a strike through. These can't be adjusted, but they can be deleted if you accidentally submit a true positive as a false positive from Frigate.
|
||||
@@ -51,3 +57,12 @@ Misidentified objects should have a correct label added. For example, if a perso
|
||||
| `scrollwheel` | Zoom in/out |
|
||||
| `f` | Hide/show all but current box |
|
||||
| `spacebar` | Verify and save |
|
||||
|
||||
## Deleting images
|
||||
|
||||
In most cases, you should keep your existing images. Removing training data usually makes accuracy worse, not better, so you shouldn't delete images just because you think you made some annotation mistakes. Deleting images makes sense when you move to a new location, or significantly upgrade or reposition your cameras.
|
||||
|
||||
- To delete all images for a camera, use the **Delete Images** button for that camera on the Cameras page.
|
||||
- To delete all images in your account, use **Delete All Images** on the Settings page.
|
||||
|
||||
Deletions run in the background, and their progress is shown in the bulk operations panel. Deleted images cannot be recovered and are no longer used to train your models.
|
||||
|
||||
@@ -26,7 +26,9 @@ Yes. Subscriptions to Frigate+ provide access to the infrastructure used to trai
|
||||
### Can I use Frigate+ models commercially?
|
||||
|
||||
A standard subscription covers use on camera systems you own or operate, including for your business. A shop, restaurant, warehouse, or office running Frigate+ at its own locations (including multiple locations) is exactly the kind of use the subscription is for.
|
||||
|
||||
What the standard subscription does not cover is using Frigate+ models to provide a product or service to others. If you're deploying models at your customers' sites, bundling them with hardware you sell, or running them as part of a hosted or managed service, even if your customers never receive the model files themselves, you'll need a commercial license.
|
||||
|
||||
Note that professional installers are fine under standard subscriptions when each customer holds their own Frigate+ subscription. The commercial license is for cases where your license powers your customers' sites.
|
||||
|
||||
### Why can't I submit images to Frigate+?
|
||||
|
||||
@@ -17,7 +17,7 @@ For more detailed recommendations, you can refer to the docs on [annotating](./a
|
||||
|
||||
## Step 2: Submit a model request
|
||||
|
||||
Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours.
|
||||
Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. For most hardware, `yolov9` is recommended; `mobiledet` is a legacy model type and will be removed in 2027. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours.
|
||||

|
||||
|
||||
## Step 3: Set your model
|
||||
|
||||
+26
-10
@@ -5,25 +5,41 @@ title: Models
|
||||
|
||||
<a href="https://frigate.video/plus" target="_blank" rel="nofollow">Frigate+</a> offers models trained on images submitted by Frigate+ users from their security cameras and is specifically designed for the way Frigate NVR analyzes video footage. These models offer higher accuracy with less resources. The images you upload are used to fine tune a base model trained from images uploaded by all Frigate+ users. This fine tuning process results in a model that is optimized for accuracy in your specific conditions.
|
||||
|
||||
With a subscription, 12 model trainings to fine tune your model per year are included. In addition, you will have access to any base models published while your subscription is active. If you cancel your subscription, you will retain access to any trained and base models in your account. An active subscription is required to submit model requests or purchase additional trainings. New base models are published quarterly with target dates of January 15th, April 15th, July 15th, and October 15th.
|
||||
With a subscription, 12 model trainings to fine tune your model per year are included. In addition, you will have access to any base models published while your subscription is active. If you cancel your subscription, you will retain access to any trained and base models in your account. An active subscription is required to submit model requests or purchase additional trainings. New base models are published twice per year.
|
||||
|
||||
Information on how to integrate Frigate+ with Frigate can be found in the [integration docs](../integrations/plus.md).
|
||||
|
||||
## Available model types
|
||||
|
||||
There are three model types offered in Frigate+, `mobiledet`, `yolonas`, and `yolov9`. All of these models are object detection models and are trained to detect the same set of labels [listed below](#available-label-types).
|
||||
There are three model types offered in Frigate+, `yolov9`, `yolonas`, and `mobiledet` (legacy). All of these models are object detection models and are trained to detect the same set of labels [listed below](#available-label-types).
|
||||
|
||||
Not all model types are supported by all detectors, so it's important to choose a model type to match your detector as shown in the table under [supported detector types](#supported-detector-types). You can test model types for compatibility and speed on your hardware by using the base models.
|
||||
|
||||
| Model Type | Description |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mobiledet` | Based on the same architecture as the default model included with Frigate. Runs on Google Coral devices and CPUs. |
|
||||
| `yolonas` | A newer architecture that offers slightly higher accuracy and improved detection of small objects. Runs on Intel, NVidia GPUs, and AMD GPUs. |
|
||||
| `yolov9` | A leading SOTA (state of the art) object detection model with similar performance to yolonas, but on a wider range of hardware options. Runs on most hardware. |
|
||||
| `yolonas` | A newer architecture that offers slightly higher accuracy and improved detection of small objects. Runs on Intel, NVidia GPUs, and AMD GPUs. |
|
||||
| `mobiledet` | Legacy. Based on the same architecture as the default model included with Frigate. Runs on Google Coral devices and CPUs. |
|
||||
|
||||
:::warning
|
||||
|
||||
`mobiledet` is a legacy model type. Base model 2026.3 is the last base model update for `mobiledet`, and it is planned to be removed from Frigate+ in 2027. If you are using `mobiledet` with a Google Coral, request a `yolov9` model with the `edgetpu` variant instead.
|
||||
|
||||
:::
|
||||
|
||||
### YOLOv9 Details
|
||||
|
||||
YOLOv9 models are available in `s`, `t`, `edgetpu` variants. When requesting a `yolov9` model, you will be prompted to choose a variant. If you want the model to be compatible with a Google Coral, you will need to choose the `edgetpu` variant. If you are unsure what variant to choose, you should perform some tests with the base models to find the performance level that suits you. The `s` size is most similar to the current `yolonas` models in terms of inference times and accuracy, and a good place to start is the `320x320` resolution model for `yolov9s`.
|
||||
YOLOv9 models are available in `t`, `s`, `c`, and `edgetpu` variants, and an `m` size is coming soon. When requesting a `yolov9` model, you will be prompted to choose a variant. If you want the model to be compatible with a Google Coral, you will need to choose the `edgetpu` variant, which requires Frigate 0.17 or later. If you are unsure what variant to choose, you should perform some tests with the base models to find the performance level that suits you. The `s` size is most similar to the current `yolonas` models in terms of inference times and accuracy, and a good place to start is the `320x320` resolution model for `yolov9s`. The `c` size is larger and offers higher accuracy at the cost of slower inference, so it is best suited to more powerful hardware such as GPUs.
|
||||
|
||||
| Variant | Size | Parameters | Speed and accuracy | Best suited for |
|
||||
| --------- | -------------------- | ---------- | ----------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `t` | Tiny | ~2.0M | Fastest inference, lowest accuracy | Low power hardware or a large number of cameras |
|
||||
| `s` | Small | ~7.1M | Balanced. Similar inference times and accuracy to `yolonas` | Most users. Start with `320x320` |
|
||||
| `m` | Medium (coming soon) | ~20.0M | Slower than `s` with higher accuracy | Mid-range GPUs and NPUs |
|
||||
| `c` | Compact | ~25.3M | Highest accuracy, slowest inference | More powerful hardware such as GPUs |
|
||||
| `edgetpu` | N/A | N/A | Optimized to run on the Coral's Edge TPU | Google Coral devices only (requires Frigate 0.17+) |
|
||||
|
||||
Parameter counts are for the standard YOLOv9 architectures. Actual inference times depend on your hardware and the input resolution you choose, so test with the base models before requesting a model.
|
||||
|
||||
:::info
|
||||
|
||||
@@ -33,7 +49,7 @@ When switching to YOLOv9, you may need to adjust your thresholds for some object
|
||||
|
||||
#### Hailo Support
|
||||
|
||||
If you have a Hailo device, you will need to specify the hardware you have when submitting a model request because they are not cross compatible. Please test using the available base models before submitting your model request.
|
||||
If you have a Hailo device, you will need to specify the hardware you have when submitting a model request because they are not cross compatible. Hailo models are available for the `t`, `s`, and `c` variants on Hailo8, Hailo8L, and Hailo8R devices. Please test using the available base models before submitting your model request.
|
||||
|
||||
#### Rockchip (RKNN) Support
|
||||
|
||||
@@ -44,8 +60,8 @@ Rockchip models are automatically converted as of 0.17. For 0.16, YOLOv9 onnx mo
|
||||
Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo8l`), and Rockchip (`rknn`) detectors.
|
||||
|
||||
| Hardware | Recommended Detector Type | Recommended Model Type |
|
||||
| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- |
|
||||
| [CPU](/configuration/object_detectors.md#cpu-detector-not-recommended) | `cpu` | `mobiledet` |
|
||||
| -------------------------------------------------------------------------------- | --------------------------- | ---------------------- |
|
||||
| [CPU](/configuration/object_detectors.md#cpu-detector-not-recommended) | `openvino (or alternative)` | `yolov9` |
|
||||
| [Coral (all form factors)](/configuration/object_detectors.md#edge-tpu-detector) | `edgetpu` | `yolov9` |
|
||||
| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` |
|
||||
| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` |
|
||||
@@ -66,7 +82,7 @@ Some users may find that Frigate+ models result in more false positives initiall
|
||||
Frigate+ models support a more relevant set of objects for security cameras. The labels for annotation in Frigate+ are configurable by editing the camera in the Cameras section of Frigate+. Currently, the following objects are supported:
|
||||
|
||||
- **People**: `person`, `face`, `baby`
|
||||
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `garbage truck`, `license_plate`
|
||||
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `garbage_truck`, `license_plate`
|
||||
- **Delivery Logos**: `amazon`, `usps`, `ups`, `fedex`, `dhl`, `an_post`, `purolator`, `postnl`, `nzpost`, `postnord`, `gls`, `dpd`, `canada_post`, `royal_mail`
|
||||
- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo`, `possum`, `rodent`
|
||||
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`, `baby_stroller`
|
||||
@@ -81,7 +97,7 @@ Where possible, these labels are mapped to existing labels during training. For
|
||||
|
||||
- **Vehicles**: `tractor`, `golf_cart`, `bus`, `airplane`, `helicopter`, `rickshaw`, `scooter`
|
||||
- **Delivery Logos**: `bpost`, `auspost`, `aramex`, `transoflex`, `parcelforce`, `hermes`, `cargus`, `fan_courier`, `sameday`, `la_poste`
|
||||
- **Animals**: `badger`, `chicken`, `duck`, `turkey`, `groundhog`, `boar`, `hedgehog`, `wombat`, `bobcat`, `mustelid`, `mountain_lion`, `crocodile`, `monkey`, `coyote`, `porcupine`, `sheep`, `snake`, `lizard`, `heron`, `elk`, `moose`, `pig`, `donkey`, `civet`
|
||||
- **Animals**: `badger`, `chicken`, `duck`, `turkey`, `groundhog`, `boar`, `hedgehog`, `wombat`, `bobcat`, `mustelid`, `mountain_lion`, `crocodile`, `monkey`, `coyote`, `porcupine`, `sheep`, `snake`, `lizard`, `heron`, `elk`, `moose`, `pig`, `donkey`, `civet`, `chipmunk`
|
||||
- **Other**: `sports_ball`, `drone`, `lawnmower`
|
||||
|
||||
Candidate labels are not available for automatic suggestions.
|
||||
|
||||
@@ -162,7 +162,7 @@ Open one of the tracked objects in Explore → **Tracking Details**. If the dete
|
||||
What helps:
|
||||
|
||||
- **Improve the view**: even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking.
|
||||
- **Use a more accurate model**: switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
|
||||
- **Use a more accurate model**: switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t` (or `yolov9-c` on more powerful hardware), can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
|
||||
- **Don't set `detect -> stationary -> max_frames` for `car`**: it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md).
|
||||
- **Restrict alerts to the areas you care about** with `required_zones`. See [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves.
|
||||
- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc.
|
||||
|
||||
+18
-14
@@ -74,6 +74,7 @@ from frigate.util.config import (
|
||||
from frigate.util.schema import get_config_schema
|
||||
from frigate.util.services import (
|
||||
Go2rtcStreamIssue,
|
||||
find_restricted_go2rtc_exec_changes,
|
||||
find_restricted_go2rtc_stream_changes,
|
||||
get_nvidia_driver_info,
|
||||
process_logs,
|
||||
@@ -524,26 +525,26 @@ def config_raw():
|
||||
)
|
||||
|
||||
|
||||
def _go2rtc_streams(raw_config: str) -> Any:
|
||||
"""Load go2rtc.streams with the same loader Frigate and go2rtc startup use.
|
||||
def _go2rtc_config(raw_config: str) -> dict[str, Any]:
|
||||
"""Load the go2rtc section with the same loader Frigate and go2rtc startup use.
|
||||
|
||||
Raises ruamel.yaml.YAMLError when the config cannot be parsed.
|
||||
"""
|
||||
loaded = ruamel.yaml.YAML().load(raw_config)
|
||||
go2rtc = loaded.get("go2rtc") if isinstance(loaded, dict) else None
|
||||
return go2rtc.get("streams") if isinstance(go2rtc, dict) else None
|
||||
return go2rtc if isinstance(go2rtc, dict) else {}
|
||||
|
||||
|
||||
def _restricted_go2rtc_streams_response(
|
||||
def _restricted_go2rtc_response(
|
||||
old_raw_config: str, new_raw_config: str
|
||||
) -> JSONResponse | None:
|
||||
"""Reject config changes that add restricted go2rtc stream sources.
|
||||
"""Reject config changes that add restricted go2rtc sources or options.
|
||||
|
||||
The same sources are rejected by PUT /go2rtc/streams, and go2rtc would
|
||||
otherwise load them from the saved config on the next restart.
|
||||
"""
|
||||
try:
|
||||
new_streams = _go2rtc_streams(new_raw_config)
|
||||
new_go2rtc = _go2rtc_config(new_raw_config)
|
||||
except ruamel.yaml.YAMLError:
|
||||
logger.warning("Rejected config change that could not be parsed")
|
||||
return JSONResponse(
|
||||
@@ -552,16 +553,21 @@ def _restricted_go2rtc_streams_response(
|
||||
)
|
||||
|
||||
try:
|
||||
old_streams = _go2rtc_streams(old_raw_config)
|
||||
old_go2rtc = _go2rtc_config(old_raw_config)
|
||||
except ruamel.yaml.YAMLError:
|
||||
# with no readable saved config, every source is treated as new
|
||||
old_streams = None
|
||||
old_go2rtc = {}
|
||||
|
||||
issues = find_restricted_go2rtc_stream_changes(old_streams, new_streams)
|
||||
if not issues:
|
||||
issues = find_restricted_go2rtc_stream_changes(
|
||||
old_go2rtc.get("streams"), new_go2rtc.get("streams")
|
||||
)
|
||||
exec_keys = find_restricted_go2rtc_exec_changes(old_go2rtc, new_go2rtc)
|
||||
if not issues and not exec_keys:
|
||||
return None
|
||||
|
||||
messages = []
|
||||
if exec_keys:
|
||||
messages.append(f"Restricted go2rtc option: {', '.join(exec_keys)}")
|
||||
for issue, label in (
|
||||
(Go2rtcStreamIssue.restricted, "Restricted go2rtc stream source in"),
|
||||
(Go2rtcStreamIssue.unknown_variable, "Unknown variable in go2rtc stream"),
|
||||
@@ -665,9 +671,7 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
||||
except OSError:
|
||||
old_raw_config = ""
|
||||
|
||||
restricted_response = _restricted_go2rtc_streams_response(
|
||||
old_raw_config, new_config
|
||||
)
|
||||
restricted_response = _restricted_go2rtc_response(old_raw_config, new_config)
|
||||
if restricted_response is not None:
|
||||
return restricted_response
|
||||
|
||||
@@ -983,7 +987,7 @@ def config_set(request: Request, body: AppConfigSetBody):
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
restricted_response = _restricted_go2rtc_streams_response(
|
||||
restricted_response = _restricted_go2rtc_response(
|
||||
old_raw_config, new_raw_config
|
||||
)
|
||||
if restricted_response is not None:
|
||||
|
||||
@@ -2,7 +2,7 @@ from enum import Enum
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from frigate.util.config import resolve_ffmpeg_path
|
||||
from frigate.util.config import is_valid_ffmpeg_path, resolve_ffmpeg_path
|
||||
|
||||
from ..base import FrigateBaseModel
|
||||
from ..env import EnvString
|
||||
@@ -87,6 +87,16 @@ class FfmpegConfig(FrigateBaseModel):
|
||||
description="Default GPU index used for hardware acceleration if available.",
|
||||
)
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_path(cls, v: str) -> str:
|
||||
if not is_valid_ffmpeg_path(v):
|
||||
raise ValueError(
|
||||
"FFmpeg path must be a version alias or an absolute path without "
|
||||
"spaces, quotes or shell characters"
|
||||
)
|
||||
return v
|
||||
|
||||
@property
|
||||
def ffmpeg_path(self) -> str:
|
||||
return resolve_ffmpeg_path(self.path, "ffmpeg")
|
||||
|
||||
@@ -85,6 +85,23 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
|
||||
def _set_go2rtc(self, go2rtc: dict):
|
||||
with AuthTestClient(self.app) as client:
|
||||
return client.put(
|
||||
"/config/set",
|
||||
json={"config_data": {"go2rtc": go2rtc}, "requires_restart": 1},
|
||||
)
|
||||
|
||||
def _save_go2rtc(self, go2rtc: dict):
|
||||
config = copy.deepcopy(_CONFIG)
|
||||
config["go2rtc"].update(go2rtc)
|
||||
with AuthTestClient(self.app) as client:
|
||||
return client.post(
|
||||
"/config/save?save_option=saveonly",
|
||||
content=_dump(config),
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
|
||||
# --- config/set ---
|
||||
|
||||
def test_set_rejects_injected_ffmpeg_source(self):
|
||||
@@ -215,3 +232,84 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
|
||||
resp = self._set_streams({"new_stream": [src]})
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert src in self._read_config()
|
||||
|
||||
# --- go2rtc ffmpeg and ngrok options ---
|
||||
|
||||
def test_save_rejects_ffmpeg_bin_override(self):
|
||||
before = self._read_config()
|
||||
resp = self._save_go2rtc({"ffmpeg": {"bin": "/bin/sh", "global": "-c 'id' x"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["message"] == (
|
||||
"Restricted go2rtc option: ffmpeg.bin, ffmpeg.global"
|
||||
)
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_set_rejects_ffmpeg_bin_override(self):
|
||||
before = self._read_config()
|
||||
resp = self._set_go2rtc({"ffmpeg": {"bin": "/bin/sh"}})
|
||||
assert resp.status_code == 400
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_save_rejects_ffmpeg_template_override(self):
|
||||
before = self._read_config()
|
||||
resp = self._save_go2rtc({"ffmpeg": {"rtsp": "-i {input} -y /config/x"}})
|
||||
assert resp.status_code == 400
|
||||
assert "ffmpeg.rtsp" in resp.json()["message"]
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_set_rejects_custom_ffmpeg_template(self):
|
||||
# a stream can reference any template key with #video=<name>
|
||||
before = self._read_config()
|
||||
resp = self._set_go2rtc({"ffmpeg": {"evil": "-y /config/x"}})
|
||||
assert resp.status_code == 400
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_save_rejects_ngrok(self):
|
||||
before = self._read_config()
|
||||
resp = self._save_go2rtc({"ngrok": {"command": "/bin/sh -c id"}})
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["message"] == "Restricted go2rtc option: ngrok.command"
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_set_rejects_ngrok(self):
|
||||
before = self._read_config()
|
||||
resp = self._set_go2rtc({"ngrok": {"command": "/bin/sh -c id"}})
|
||||
assert resp.status_code == 400
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_save_keeps_existing_ffmpeg_template_on_disk(self):
|
||||
go2rtc = {"ffmpeg": {"global": "-vaapi_device /dev/dri/renderD128"}}
|
||||
config = copy.deepcopy(_CONFIG)
|
||||
config["go2rtc"].update(go2rtc)
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(_dump(config))
|
||||
|
||||
resp = self._save_go2rtc(go2rtc)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
resp = self._save_go2rtc({"ffmpeg": {"global": "-hide_banner"}})
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_exec_options_allowed_when_override_set(self):
|
||||
with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}):
|
||||
resp = self._save_go2rtc(
|
||||
{
|
||||
"ffmpeg": {"bin": "/usr/bin/ffmpeg", "rtsp": "-i {input}"},
|
||||
"ngrok": {"command": "ngrok tcp 8555"},
|
||||
}
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert "ngrok tcp 8555" in self._read_config()
|
||||
|
||||
def test_save_rejects_unsafe_ffmpeg_path(self):
|
||||
before = self._read_config()
|
||||
config = copy.deepcopy(_CONFIG)
|
||||
config["ffmpeg"] = {"path": "/bin/sh -c id;#"}
|
||||
with AuthTestClient(self.app) as client:
|
||||
resp = client.post(
|
||||
"/config/save?save_option=saveonly",
|
||||
content=_dump(config),
|
||||
headers={"Content-Type": "text/plain"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert self._read_config() == before
|
||||
|
||||
@@ -11,6 +11,7 @@ from frigate.config import BirdseyeModeEnum, FrigateConfig
|
||||
from frigate.const import MODEL_CACHE_DIR
|
||||
from frigate.detectors import DetectorTypeEnum
|
||||
from frigate.util.builtin import deep_merge
|
||||
from frigate.util.config import resolve_ffmpeg_path
|
||||
|
||||
|
||||
class TestConfig(unittest.TestCase):
|
||||
@@ -1765,5 +1766,54 @@ class TestAttributeFilterDefaults(unittest.TestCase):
|
||||
self.assertEqual(face_filter.min_score, 0.3)
|
||||
|
||||
|
||||
class TestFfmpegPath(unittest.TestCase):
|
||||
# ffmpeg commands are split on spaces, so this would run /bin/sh
|
||||
UNSAFE_PATH = "/bin/sh -c id;#"
|
||||
|
||||
def setUp(self):
|
||||
self.minimal = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def test_custom_path_allowed(self):
|
||||
config = deep_merge(
|
||||
{"ffmpeg": {"path": "/usr/lib/jellyfin-ffmpeg"}}, self.minimal
|
||||
)
|
||||
frigate_config = FrigateConfig(**config)
|
||||
self.assertEqual(
|
||||
frigate_config.cameras["back"].ffmpeg.ffmpeg_path,
|
||||
"/usr/lib/jellyfin-ffmpeg/bin/ffmpeg",
|
||||
)
|
||||
|
||||
def test_unsafe_global_path_rejected(self):
|
||||
for path in (self.UNSAFE_PATH, "/opt/my ffmpeg", "/opt/'x'", "/opt/x\n"):
|
||||
config = deep_merge({"ffmpeg": {"path": path}}, self.minimal)
|
||||
with self.assertRaises(ValidationError, msg=path):
|
||||
FrigateConfig(**config)
|
||||
|
||||
def test_unsafe_camera_path_rejected(self):
|
||||
config = deep_merge(
|
||||
{"cameras": {"back": {"ffmpeg": {"path": self.UNSAFE_PATH}}}},
|
||||
self.minimal,
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
FrigateConfig(**config)
|
||||
|
||||
def test_resolve_falls_back_for_unsafe_path(self):
|
||||
self.assertEqual(
|
||||
resolve_ffmpeg_path(self.UNSAFE_PATH), resolve_ffmpeg_path("default")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Tests for the go2rtc ffmpeg and ngrok options applied at startup."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from frigate.util.services import apply_go2rtc_ffmpeg_config
|
||||
|
||||
FFMPEG_PATH = "/usr/lib/ffmpeg/7.0/bin/ffmpeg"
|
||||
|
||||
|
||||
class TestApplyGo2rtcFfmpegConfig(unittest.TestCase):
|
||||
def setUp(self):
|
||||
env_patch = patch.dict(os.environ)
|
||||
env_patch.start()
|
||||
self.addCleanup(env_patch.stop)
|
||||
os.environ.pop("GO2RTC_ALLOW_ARBITRARY_EXEC", None)
|
||||
|
||||
def test_sets_bin_when_missing(self):
|
||||
config = {}
|
||||
assert apply_go2rtc_ffmpeg_config(config, FFMPEG_PATH) == []
|
||||
assert config["ffmpeg"] == {"bin": FFMPEG_PATH}
|
||||
|
||||
def test_forces_bin_override(self):
|
||||
config = {"ffmpeg": {"bin": "/bin/sh", "global": "-c 'id' x"}}
|
||||
assert apply_go2rtc_ffmpeg_config(config, FFMPEG_PATH) == ["ffmpeg.bin"]
|
||||
assert config["ffmpeg"]["bin"] == FFMPEG_PATH
|
||||
|
||||
def test_keeps_saved_templates(self):
|
||||
config = {"ffmpeg": {"global": "-vaapi_device /dev/dri/renderD128"}}
|
||||
assert apply_go2rtc_ffmpeg_config(config, FFMPEG_PATH) == []
|
||||
assert config["ffmpeg"]["global"] == "-vaapi_device /dev/dri/renderD128"
|
||||
|
||||
def test_drops_ngrok(self):
|
||||
config = {"ngrok": {"command": "/bin/sh -c id"}}
|
||||
assert apply_go2rtc_ffmpeg_config(config, FFMPEG_PATH) == ["ngrok"]
|
||||
assert "ngrok" not in config
|
||||
|
||||
def test_allows_bin_and_ngrok_when_override_set(self):
|
||||
config = {
|
||||
"ffmpeg": {"bin": "/usr/bin/ffmpeg"},
|
||||
"ngrok": {"command": "ngrok tcp 8555"},
|
||||
}
|
||||
with patch.dict(os.environ, {"GO2RTC_ALLOW_ARBITRARY_EXEC": "true"}):
|
||||
assert apply_go2rtc_ffmpeg_config(config, FFMPEG_PATH) == []
|
||||
assert config["ffmpeg"]["bin"] == "/usr/bin/ffmpeg"
|
||||
assert config["ngrok"] == {"command": "ngrok tcp 8555"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+16
-3
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
@@ -23,6 +24,15 @@ logger = logging.getLogger(__name__)
|
||||
CURRENT_CONFIG_VERSION = "0.18-0"
|
||||
DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml")
|
||||
|
||||
# ffmpeg commands are built as strings and split on whitespace, so a custom path
|
||||
# must stay a single plain token
|
||||
_CUSTOM_FFMPEG_PATH = re.compile(r"/[\w.+@/-]+")
|
||||
|
||||
|
||||
def is_valid_ffmpeg_path(path: str) -> bool:
|
||||
"""Check that a custom ffmpeg path has no whitespace, quotes or shell syntax."""
|
||||
return not path.startswith("/") or _CUSTOM_FFMPEG_PATH.fullmatch(path) is not None
|
||||
|
||||
|
||||
def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
|
||||
"""Resolve an ffmpeg version alias or custom path to a binary path.
|
||||
@@ -30,10 +40,13 @@ def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
|
||||
A bare version alias that is no longer bundled (for example one that was
|
||||
dropped when the default version changed) falls back to the default
|
||||
bundled version so existing configs keep working across an upgrade or a
|
||||
revert. Custom install paths (anything absolute) are used as-is.
|
||||
revert. Custom install paths (anything absolute) are used as-is, unless
|
||||
they fail `is_valid_ffmpeg_path`, which also falls back to the default.
|
||||
"""
|
||||
if path == "default" or (
|
||||
not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS
|
||||
if (
|
||||
path == "default"
|
||||
or not is_valid_ffmpeg_path(path)
|
||||
or (not path.startswith("/") and path not in INCLUDED_FFMPEG_VERSIONS)
|
||||
):
|
||||
version = DEFAULT_FFMPEG_VERSION
|
||||
elif path in INCLUDED_FFMPEG_VERSIONS:
|
||||
|
||||
@@ -1103,6 +1103,74 @@ def find_restricted_go2rtc_stream_changes(
|
||||
return issues
|
||||
|
||||
|
||||
# go2rtc runs `ffmpeg.bin` and `ngrok.command`, and every other `ffmpeg` key is a
|
||||
# template of ffmpeg arguments that `#video=<name>` and friends can reference
|
||||
_GO2RTC_EXEC_SECTIONS = ("ffmpeg", "ngrok")
|
||||
|
||||
|
||||
def find_restricted_go2rtc_exec_changes(
|
||||
old_go2rtc: dict[str, Any] | None, new_go2rtc: dict[str, Any] | None
|
||||
) -> list[str]:
|
||||
"""Return the go2rtc ffmpeg and ngrok keys added or changed through the API.
|
||||
|
||||
These are only accepted with arbitrary exec enabled. Keys already in the
|
||||
saved config are left alone, the same as stream sources.
|
||||
"""
|
||||
if is_go2rtc_arbitrary_exec_allowed():
|
||||
return []
|
||||
|
||||
old_go2rtc = old_go2rtc if isinstance(old_go2rtc, dict) else {}
|
||||
new_go2rtc = new_go2rtc if isinstance(new_go2rtc, dict) else {}
|
||||
changed: list[str] = []
|
||||
|
||||
for section in _GO2RTC_EXEC_SECTIONS:
|
||||
old = old_go2rtc.get(section)
|
||||
new = new_go2rtc.get(section)
|
||||
|
||||
if not isinstance(new, dict):
|
||||
if new is not None and new != old:
|
||||
changed.append(section)
|
||||
continue
|
||||
|
||||
old = old if isinstance(old, dict) else {}
|
||||
changed.extend(
|
||||
f"{section}.{key}" for key, value in new.items() if old.get(key) != value
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def apply_go2rtc_ffmpeg_config(
|
||||
go2rtc_config: dict[str, Any], ffmpeg_path: str
|
||||
) -> list[str]:
|
||||
"""Set go2rtc's ffmpeg binary for startup.
|
||||
|
||||
Unless arbitrary exec is enabled, `ffmpeg.bin` is forced to Frigate's ffmpeg
|
||||
and `ngrok` is removed. Templates in the saved config are kept.
|
||||
|
||||
Returns:
|
||||
The keys that were overridden or removed
|
||||
"""
|
||||
allowed = is_go2rtc_arbitrary_exec_allowed()
|
||||
restricted: list[str] = []
|
||||
|
||||
ffmpeg = go2rtc_config.get("ffmpeg")
|
||||
if not isinstance(ffmpeg, dict):
|
||||
ffmpeg = go2rtc_config["ffmpeg"] = {}
|
||||
|
||||
if ffmpeg.get("bin") is None:
|
||||
ffmpeg["bin"] = ffmpeg_path
|
||||
elif ffmpeg["bin"] != ffmpeg_path and not allowed:
|
||||
restricted.append("ffmpeg.bin")
|
||||
ffmpeg["bin"] = ffmpeg_path
|
||||
|
||||
if "ngrok" in go2rtc_config and not allowed:
|
||||
restricted.append("ngrok")
|
||||
del go2rtc_config["ngrok"]
|
||||
|
||||
return restricted
|
||||
|
||||
|
||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||
"""Run ffprobe on stream."""
|
||||
clean_path = escape_special_characters(path)
|
||||
|
||||
Reference in New Issue
Block a user