Compare commits

..
Author SHA1 Message Date
Blake BlackshearandGitHub 93016c662f add synaptics to release (#21591)
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
2026-01-10 11:50:37 -06:00
c08fa15724 Miscellaneous Fixes (0.17 beta) (#21575)
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
* icon improvements

add type to getIconForLabel
provide default icon for audio events

* Add preferred language to review docs

* prevent react Suspense crash during auth redirect

add redirect-check guards to stop rendering lazy routes while navigation is pending (fixes some users seeing React error #426 when auth expires)

* Uppsercase model name

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-09 16:23:33 -07:00
GuoQing LiuandGitHub f3543cfee2 I18N Miscellaneous Fixes (#21573)
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
* fix: fix classification none tag i18n wrong

* fix: fix set password dialog jwt time i18n wrong

* fix: fix wizard other camera i18n

* fix: fix explore tracking detail audio i18n

* feat: add system processes info i18n

* fix: fix live page label i18n
2026-01-08 14:28:18 -07:00
74d14cb8ca Miscellaneous Fixes (0.17 beta) (#21558)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
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
CI / AMD64 Extra Build (push) Blocked by required conditions
* mse player improvements

- fix WebSocket race condition by registering message handlers before sending and avoid closing CONNECTING sockets to eliminate "Socket is not connected" errors.
- attempt to resolve Safari MSE timeout and handler issues by wrapping temporary handlers in try/catch and stabilizing the permanent mse handler so SourceBuffer setup completes reliably.
- add intentional disconnect tracking to prevent unwanted reconnects during navigation/StrictMode cycles

* Update Ollama

* additional MSE tweaks

* Turn activity context prompt into a yaml example

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-01-07 17:29:19 -06:00
Sai Bharat KumarandGitHub 99d48ecbc3 docs: fix alt text and capitalization in documentation (#21551)
CI / Synaptics Build (push) Blocked by required conditions
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Assemble and push default build (push) Blocked by required conditions
- Fix incorrect alt text in README.md for mask and zone editor screenshot

- Capitalize 'Frigate' in audio_detectors.md for consistency
2026-01-07 07:22:05 -07:00
Nicolas MowenandGitHub c8f55ac41f Restrict go2rtc exec sources by default (#21543)
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
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
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
* Restrict go2rtc exec sources by default

* add docs
2026-01-06 09:07:51 -06:00
047ae19191 Miscellaneous fixes (0.17 Beta) (#21489)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Correctly set query padding

* Adjust AMD headers and add community badge

* Simplify getting started guide for camera wizard

* add optimizing performance guide

* tweaks

* fix character issue

* fix more characters

* fix links

* fix more links

* Refactor new docs

* Add import

* Fix link

* Don't list hardware

* Reduce redundancy in titles

* Add note about Intel NPU and addon

* Fix ability to specify if card is using heading

* improve display of area percentage

* fix text color on genai summary chip

* fix indentation in genai docs

* Adjust default config model to align with recommended

* add correct genai key

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-01-04 12:12:03 -06:00
Blake BlackshearandGitHub d1f28eb8e1 llc to inc and 2025 to 2026 (#21484)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
2026-01-01 09:56:09 -06:00
36 changed files with 427 additions and 146 deletions
+2 -2
View File
@@ -39,14 +39,14 @@ jobs:
STABLE_TAG=${BASE}:stable
PULL_TAG=${BASE}:${BUILD_TAG}
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${VERSION_TAG}
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${VERSION_TAG}-${variant}
done
# stable tag
if [[ "${BUILD_TYPE}" == "stable" ]]; then
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG} docker://${STABLE_TAG}
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm; do
for variant in standard-arm64 tensorrt tensorrt-jp6 rk rocm synaptics; do
docker run --rm -v $HOME/.docker/config.json:/config.json quay.io/skopeo/stable:latest copy --authfile /config.json --multi-arch all docker://${PULL_TAG}-${variant} docker://${STABLE_TAG}-${variant}
done
fi
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2025 Frigate LLC (Frigate™)
Copyright (c) 2026 Frigate, Inc. (Frigate™)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+3 -3
View File
@@ -40,7 +40,7 @@ If you would like to make a donation to support development, please use [Github
This project is licensed under the **MIT License**.
- **Code:** The source code, configuration files, and documentation in this repository are available under the [MIT License](LICENSE). You are free to use, modify, and distribute the code as long as you include the original copyright notice.
- **Trademarks:** The "Frigate" name, the "Frigate NVR" brand, and the Frigate logo are **trademarks of Frigate LLC** and are **not** covered by the MIT License.
- **Trademarks:** The "Frigate" name, the "Frigate NVR" brand, and the Frigate logo are **trademarks of Frigate, Inc.** and are **not** covered by the MIT License.
Please see our [Trademark Policy](TRADEMARK.md) for details on acceptable use of our brand assets.
@@ -67,7 +67,7 @@ Please see our [Trademark Policy](TRADEMARK.md) for details on acceptable use of
### Built-in mask and zone editor
<div>
<img width="800" alt="Multi-camera scrubbing" src="https://github.com/blakeblackshear/frigate/assets/569905/d7885fc3-bfe6-452f-b7d0-d957cb3e31f5">
<img width="800" alt="Built-in mask and zone editor" src="https://github.com/blakeblackshear/frigate/assets/569905/d7885fc3-bfe6-452f-b7d0-d957cb3e31f5">
</div>
## Translations
@@ -80,4 +80,4 @@ We use [Weblate](https://hosted.weblate.org/projects/frigate-nvr/) to support la
---
**Copyright © 2025 Frigate LLC.**
**Copyright © 2026 Frigate, Inc.**
+2 -2
View File
@@ -41,7 +41,7 @@
**代码部分**:本代码库中的源代码、配置文件和文档均遵循 [MIT 许可证](LICENSE)。您可以自由使用、修改和分发这些代码,但必须保留原始版权声明。
**商标部分**:“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate LLC 的商标**,**不在** MIT 许可证覆盖范围内。
**商标部分**:“Frigate”名称、“Frigate NVR”品牌以及 Frigate 的 Logo 为 **Frigate, Inc. 的商标**,**不在** MIT 许可证覆盖范围内。
有关品牌资产的规范使用详情,请参阅我们的[《商标政策》](TRADEMARK.md)。
## 截图
@@ -87,4 +87,4 @@ Bilibilihttps://space.bilibili.com/3546894915602564
---
**Copyright © 2025 Frigate LLC.**
**Copyright © 2026 Frigate, Inc.**
+4 -4
View File
@@ -6,7 +6,7 @@ This document outlines the policy regarding the use of the trademarks associated
## 1. Our Trademarks
The following terms and visual assets are trademarks (the "Marks") of **Frigate LLC**:
The following terms and visual assets are trademarks (the "Marks") of **Frigate, Inc.**:
- **Frigate™**
- **Frigate NVR™**
@@ -14,7 +14,7 @@ The following terms and visual assets are trademarks (the "Marks") of **Frigate
- **The Frigate Logo**
**Note on Common Law Rights:**
Frigate LLC asserts all common law rights in these Marks. The absence of a federal registration symbol (®) does not constitute a waiver of our intellectual property rights.
Frigate, Inc. asserts all common law rights in these Marks. The absence of a federal registration symbol (®) does not constitute a waiver of our intellectual property rights.
## 2. Interaction with the MIT License
@@ -25,7 +25,7 @@ The software in this repository is licensed under the [MIT License](LICENSE).
- The **Code** is free to use, modify, and distribute under the MIT terms.
- The **Brand (Trademarks)** is **NOT** licensed under MIT.
You may not use the Marks in any way that is not explicitly permitted by this policy or by written agreement with Frigate LLC.
You may not use the Marks in any way that is not explicitly permitted by this policy or by written agreement with Frigate, Inc.
## 3. Acceptable Use
@@ -40,7 +40,7 @@ You may use the Marks without prior written permission in the following specific
You may **NOT** use the Marks in the following ways:
- **Commercial Products:** You may not use "Frigate" in the name of a commercial product, service, or app (e.g., selling an app named _"Frigate Viewer"_ is prohibited).
- **Implying Affiliation:** You may not use the Marks in a way that suggests your project is official, sponsored by, or endorsed by Frigate LLC.
- **Implying Affiliation:** You may not use the Marks in a way that suggests your project is official, sponsored by, or endorsed by Frigate, Inc.
- **Confusing Forks:** If you fork this repository to create a derivative work, you **must** remove the Frigate logo and rename your project to avoid user confusion. You cannot distribute a modified version of the software under the name "Frigate".
- **Domain Names:** You may not register domain names containing "Frigate" that are likely to confuse users (e.g., `frigate-official-support.com`).
+1 -1
View File
@@ -48,7 +48,7 @@ onnxruntime == 1.22.*
transformers == 4.45.*
# Generative AI
google-generativeai == 0.8.*
ollama == 0.5.*
ollama == 0.6.*
openai == 1.65.*
# push notifications
py-vapid == 1.9.*
@@ -22,6 +22,11 @@ sys.path.remove("/opt/frigate")
yaml = YAML()
# Check if arbitrary exec sources are allowed (defaults to False for security)
ALLOW_ARBITRARY_EXEC = os.environ.get(
"GO2RTC_ALLOW_ARBITRARY_EXEC", "false"
).lower() in ("true", "1", "yes")
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
@@ -109,14 +114,26 @@ if LIBAVFORMAT_VERSION_MAJOR < 59:
elif go2rtc_config["ffmpeg"].get("rtsp") is None:
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
for name in go2rtc_config.get("streams", {}):
def is_restricted_source(stream_source: str) -> bool:
"""Check if a stream source is restricted (echo, expr, or exec)."""
return stream_source.strip().startswith(("echo:", "expr:", "exec:"))
for name in list(go2rtc_config.get("streams", {})):
stream = go2rtc_config["streams"][name]
if isinstance(stream, str):
try:
go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format(
**FRIGATE_ENV_VARS
)
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
)
del go2rtc_config["streams"][name]
continue
go2rtc_config["streams"][name] = formatted_stream
except KeyError as e:
print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
@@ -124,15 +141,33 @@ for name in go2rtc_config.get("streams", {}):
sys.exit(e)
elif isinstance(stream, list):
for i, stream in enumerate(stream):
filtered_streams = []
for i, stream_item in enumerate(stream):
try:
go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS)
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
if not ALLOW_ARBITRARY_EXEC and is_restricted_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
)
continue
filtered_streams.append(formatted_stream)
except KeyError as e:
print(
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
)
sys.exit(e)
if filtered_streams:
go2rtc_config["streams"][name] = filtered_streams
else:
print(
f"[ERROR] Stream '{name}' was removed because all sources were restricted (echo/expr/exec). "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
)
del go2rtc_config["streams"][name]
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, Any] = config.get("birdseye")
+1 -1
View File
@@ -50,7 +50,7 @@ cameras:
### Configuring Minimum Volume
The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection.
The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection.
:::tip
+1 -1
View File
@@ -211,7 +211,7 @@ You are also able to define custom prompts in your configuration.
genai:
provider: ollama
base_url: http://localhost:11434
model: llava
model: qwen3-vl:8b-instruct
objects:
prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance."
+6 -5
View File
@@ -39,13 +39,14 @@ You are also able to define custom prompts in your configuration.
genai:
provider: ollama
base_url: http://localhost:11434
model: llava
model: qwen3-vl:8b-instruct
objects:
prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance."
object_prompts:
person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details."
car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company."
genai:
prompt: "Analyze the {label} in these images from the {camera} security camera. Focus on the actions, behavior, and potential intent of the {label}, rather than just describing its appearance."
object_prompts:
person: "Examine the main person in these images. What are they doing and what might their actions suggest about their intent (e.g., approaching a door, leaving an area, standing still)? Do not describe the surroundings or static details."
car: "Observe the primary vehicle in these images. Focus on its movement, direction, or purpose (e.g., parking, approaching, circling). If it's a delivery vehicle, mention the company."
```
Prompts can also be overridden at the camera level to provide a more detailed prompt to the model about your specific camera, if you desire.
@@ -31,40 +31,43 @@ Each installation and even camera can have different parameters for what is cons
<details>
<summary>Default Activity Context Prompt</summary>
```
### Normal Activity Indicators (Level 0)
- Known/verified people in any zone at any time
- People with pets in residential areas
- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving
- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime
- Activity confined to public areas only (sidewalks, streets) without entering property at any time
```yaml
review:
genai:
activity_context_prompt: |
### Normal Activity Indicators (Level 0)
- Known/verified people in any zone at any time
- People with pets in residential areas
- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving
- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime
- Activity confined to public areas only (sidewalks, streets) without entering property at any time
### Suspicious Activity Indicators (Level 1)
- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration
- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration
- Taking items that don't belong to them (packages, objects from porches/driveways)
- Climbing or jumping fences/barriers to access property
- Attempting to conceal actions or items from view
- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence
### Suspicious Activity Indicators (Level 1)
- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration
- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration
- Taking items that don't belong to them (packages, objects from porches/driveways)
- Climbing or jumping fences/barriers to access property
- Attempting to conceal actions or items from view
- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence
### Critical Threat Indicators (Level 2)
- Holding break-in tools (crowbars, pry bars, bolt cutters)
- Weapons visible (guns, knives, bats used aggressively)
- Forced entry in progress
- Physical aggression or violence
- Active property damage or theft in progress
### Critical Threat Indicators (Level 2)
- Holding break-in tools (crowbars, pry bars, bolt cutters)
- Weapons visible (guns, knives, bats used aggressively)
- Forced entry in progress
- Physical aggression or violence
- Active property damage or theft in progress
### Assessment Guidance
Evaluate in this order:
### Assessment Guidance
Evaluate in this order:
1. **If person is verified/known** → Level 0 regardless of time or activity
2. **If person is unidentified:**
- Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1
- Check actions: If testing doors/handles, taking items, climbing → Level 1
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0
3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)
1. **If person is verified/known** → Level 0 regardless of time or activity
2. **If person is unidentified:**
- Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1
- Check actions: If testing doors/handles, taking items, climbing → Level 1
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0
3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)
The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.
The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.
```
</details>
@@ -109,6 +112,17 @@ review:
- animals in the garden
```
### Preferred Language
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
review:
genai:
enabled: true
preferred_language: Spanish
```
## Review Reports
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
+25
View File
@@ -185,10 +185,35 @@ In this configuration:
- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access.
- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel.
## Security: Restricted Stream Sources
For security reasons, the `echo:`, `expr:`, and `exec:` stream sources are disabled by default in go2rtc. These sources allow arbitrary command execution and can pose security risks if misconfigured.
If you attempt to use these sources in your configuration, the streams will be removed and an error message will be printed in the logs.
To enable these sources, you must set the environment variable `GO2RTC_ALLOW_ARBITRARY_EXEC=true`. This can be done in your Docker Compose file or container environment:
```yaml
environment:
- GO2RTC_ALLOW_ARBITRARY_EXEC=true
```
:::warning
Enabling arbitrary exec sources allows execution of arbitrary commands through go2rtc stream configurations. Only enable this if you understand the security implications and trust all sources of your configuration.
:::
## Advanced Restream Configurations
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
:::warning
The `exec:`, `echo:`, and `expr:` sources are disabled by default for security. You must set `GO2RTC_ALLOW_ARBITRARY_EXEC=true` to use them. See [Security: Restricted Stream Sources](#security-restricted-stream-sources) for more information.
:::
NOTE: The output will need to be passed with two curly braces `{{output}}`
```yaml
+1 -1
View File
@@ -170,7 +170,7 @@ const config: Config = {
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Frigate LLC`,
copyright: `Copyright © ${new Date().getFullYear()} Frigate, Inc.`,
},
},
plugins: [
+5 -5
View File
@@ -1,12 +1,12 @@
# COPYRIGHT AND TRADEMARK NOTICE
The images, logos, and icons contained in this directory (the "Brand Assets") are
proprietary to Frigate LLC and are NOT covered by the MIT License governing the
proprietary to Frigate, Inc. and are NOT covered by the MIT License governing the
rest of this repository.
1. TRADEMARK STATUS
The "Frigate" name and the accompanying logo are common law trademarks™ of
Frigate LLC. Frigate LLC reserves all rights to these marks.
Frigate, Inc. Frigate, Inc. reserves all rights to these marks.
2. LIMITED PERMISSION FOR USE
Permission is hereby granted to display these Brand Assets strictly for the
@@ -17,9 +17,9 @@ rest of this repository.
3. RESTRICTIONS
You may NOT:
a. Use these Brand Assets to represent a derivative work (fork) as an official
product of Frigate LLC.
product of Frigate, Inc.
b. Use these Brand Assets in a way that implies endorsement, sponsorship, or
commercial affiliation with Frigate LLC.
commercial affiliation with Frigate, Inc.
c. Modify or alter the Brand Assets.
If you fork this repository with the intent to distribute a modified or competing
@@ -27,4 +27,4 @@ version of the software, you must replace these Brand Assets with your own
original content.
ALL RIGHTS RESERVED.
Copyright (c) 2025 Frigate LLC.
Copyright (c) 2026 Frigate, Inc.
+5 -5
View File
@@ -1,12 +1,12 @@
# COPYRIGHT AND TRADEMARK NOTICE
The images, logos, and icons contained in this directory (the "Brand Assets") are
proprietary to Frigate LLC and are NOT covered by the MIT License governing the
proprietary to Frigate, Inc. and are NOT covered by the MIT License governing the
rest of this repository.
1. TRADEMARK STATUS
The "Frigate" name and the accompanying logo are common law trademarks™ of
Frigate LLC. Frigate LLC reserves all rights to these marks.
Frigate, Inc. Frigate, Inc. reserves all rights to these marks.
2. LIMITED PERMISSION FOR USE
Permission is hereby granted to display these Brand Assets strictly for the
@@ -17,9 +17,9 @@ rest of this repository.
3. RESTRICTIONS
You may NOT:
a. Use these Brand Assets to represent a derivative work (fork) as an official
product of Frigate LLC.
product of Frigate, Inc.
b. Use these Brand Assets in a way that implies endorsement, sponsorship, or
commercial affiliation with Frigate LLC.
commercial affiliation with Frigate, Inc.
c. Modify or alter the Brand Assets.
If you fork this repository with the intent to distribute a modified or competing
@@ -30,4 +30,4 @@ For full usage guidelines, strictly see the TRADEMARK.md file in the
repository root.
ALL RIGHTS RESERVED.
Copyright (c) 2025 Frigate LLC.
Copyright (c) 2026 Frigate, Inc.
+2 -1
View File
@@ -101,7 +101,8 @@
"show": "Show {{item}}",
"ID": "ID",
"none": "None",
"all": "All"
"all": "All",
"other": "Other"
},
"list": {
"two": "{{0}} and {{1}}",
+8 -1
View File
@@ -86,7 +86,14 @@
"otherProcesses": {
"title": "Other Processes",
"processCpuUsage": "Process CPU Usage",
"processMemoryUsage": "Process Memory Usage"
"processMemoryUsage": "Process Memory Usage",
"series": {
"go2rtc": "go2rtc",
"recording": "recording",
"review_segment": "review segment",
"embeddings": "embeddings",
"audio_detector": "audio detector"
}
}
},
"storage": {
+11
View File
@@ -15,6 +15,7 @@ import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { isRedirectingToLogin } from "@/api/auth-redirect";
const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
@@ -58,6 +59,16 @@ function DefaultAppView() {
? Object.keys(config.auth.roles)
: undefined;
// Show loading indicator during redirect to prevent React from attempting to render
// lazy components, which would cause error #426 (suspension during synchronous navigation)
if (isRedirectingToLogin()) {
return (
<div className="size-full overflow-hidden">
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
);
}
return (
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
@@ -28,6 +28,14 @@ export default function ProtectedRoute({
}
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
// Show loading indicator during redirect to prevent React from attempting to render
// lazy components, which would cause error #426 (suspension during synchronous navigation)
if (isRedirectingToLogin()) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
);
}
if (auth.isLoading) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
@@ -166,7 +166,7 @@ export const ClassificationCard = forwardRef<
<div className="break-all smart-capitalize">
{data.name == "unknown"
? t("details.unknown")
: data.name == "none"
: data.name.toLowerCase() == "none"
? t("details.none")
: data.name}
</div>
+2 -2
View File
@@ -181,7 +181,7 @@ export default function ReviewCard({
key={`${object}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(object, "size-3 text-white")}
{getIconForLabel(object, "object", "size-3 text-white")}
</div>
))}
{event.data.audio.map((audio, idx) => (
@@ -189,7 +189,7 @@ export default function ReviewCard({
key={`${audio}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(audio, "size-3 text-white")}
{getIconForLabel(audio, "audio", "size-3 text-white")}
</div>
))}
</div>
+5 -1
View File
@@ -133,7 +133,11 @@ export default function SearchThumbnail({
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
onClick={() => onClick(searchResult, false, true)}
>
{getIconForLabel(objectLabel, "size-3 text-white")}
{getIconForLabel(
objectLabel,
searchResult.data.type,
"size-3 text-white",
)}
{Math.floor(
(searchResult.data.score ??
searchResult.data.top_score ??
@@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import useSWR from "swr";
import { formatSecondsToDuration } from "@/utils/dateUtil";
import { useDateLocale } from "@/hooks/use-date-locale";
import ActivityIndicator from "../indicators/activity-indicator";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
@@ -48,12 +49,13 @@ export default function SetPasswordDialog({
const { t } = useTranslation(["views/settings", "common"]);
const { getLocaleDocUrl } = useDocDomain();
const isAdmin = useIsAdmin();
const dateLocale = useDateLocale();
const { data: config } = useSWR("config");
const refreshSeconds: number | undefined =
config?.auth?.refresh_time ?? undefined;
const refreshTimeLabel = refreshSeconds
? formatSecondsToDuration(refreshSeconds)
? formatSecondsToDuration(refreshSeconds, dateLocale)
: t("time.30minutes", { ns: "common" });
// visibility toggles for password fields
@@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
{t("details.label")}
</div>
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
{getIconForLabel(search.label, "size-4 text-primary")}
{getIconForLabel(
search.label,
search.data.type,
"size-4 text-primary",
)}
{getTranslatedLabel(search.label, search.data.type)}
{search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && (
@@ -266,7 +266,7 @@ export function TrackingDetails({
const label = event.sub_label
? event.sub_label
: getTranslatedLabel(event.label);
: getTranslatedLabel(event.label, event.data.type);
const getZoneColor = useCallback(
(zoneName: string) => {
@@ -665,6 +665,7 @@ export function TrackingDetails({
>
{getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-4 text-white",
)}
</div>
@@ -998,7 +999,7 @@ function LifecycleIconRow({
<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="whitespace-nowrap">{formattedEventTimestamp}</div>
{((isAdmin && config?.plus?.enabled) || item.data.box) && (
{isAdmin && config?.plus?.enabled && item.data.box && (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger>
<div className="rounded p-1 pr-2" role="button">
+23 -16
View File
@@ -16,7 +16,6 @@ import {
} from "@/types/live";
import { getIconForLabel } from "@/utils/iconUtil";
import Chip from "../indicators/Chip";
import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { cn } from "@/lib/utils";
import { TbExclamationCircle } from "react-icons/tb";
import { TooltipPortal } from "@radix-ui/react-tooltip";
@@ -26,6 +25,8 @@ import { LuVideoOff } from "react-icons/lu";
import { Trans, useTranslation } from "react-i18next";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay";
import { getTranslatedLabel } from "@/utils/i18n";
import { formatList } from "@/utils/stringUtil";
type LivePlayerProps = {
cameraRef?: (ref: HTMLDivElement | null) => void;
@@ -358,7 +359,11 @@ export default function LivePlayer({
]),
]
.map((label) => {
return getIconForLabel(label, "size-3 text-white");
return getIconForLabel(
label,
"object",
"size-3 text-white",
);
})
.sort()}
</Chip>
@@ -367,20 +372,22 @@ export default function LivePlayer({
</div>
<TooltipPortal>
<TooltipContent className="smart-capitalize">
{[
...new Set([
...(objects || []).map(({ label, sub_label }) =>
label.endsWith("verified")
? sub_label
: label.replaceAll("_", " "),
),
]),
]
.filter((label) => label?.includes("-verified") == false)
.map((label) => capitalizeFirstLetter(label))
.sort()
.join(", ")
.replaceAll("-verified", "")}
{formatList(
[
...new Set([
...(objects || []).map(({ label, sub_label }) =>
label.endsWith("verified")
? sub_label
: label.replaceAll("_", " "),
),
]),
]
.filter((label) => label?.includes("-verified") == false)
.map((label) =>
getTranslatedLabel(label.replace("-verified", "")),
)
.sort(),
)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
+136 -23
View File
@@ -80,12 +80,15 @@ function MSEPlayer({
const videoRef = useRef<HTMLVideoElement>(null);
const wsRef = useRef<WebSocket | null>(null);
const reconnectTIDRef = useRef<number | null>(null);
const intentionalDisconnectRef = useRef<boolean>(false);
const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null);
const onmessageRef = useRef<{
[key: string]: (msg: { value: string; type: string }) => void;
}>({});
const msRef = useRef<MediaSource | null>(null);
const mseCodecRef = useRef<string | null>(null);
const mseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const mseResponseReceivedRef = useRef<boolean>(false);
const wsURL = useMemo(() => {
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
@@ -152,8 +155,11 @@ function MSEPlayer({
}, []);
const onConnect = useCallback(() => {
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false;
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) {
return false;
}
intentionalDisconnectRef.current = false;
setWsState(WebSocket.CONNECTING);
setConnectTS(Date.now());
@@ -172,13 +178,50 @@ function MSEPlayer({
setBufferTimeout(undefined);
}
// Clear any pending MSE timeout
if (mseTimeoutRef.current !== null) {
clearTimeout(mseTimeoutRef.current);
mseTimeoutRef.current = null;
}
// Clear any pending reconnect attempts
if (reconnectTIDRef.current !== null) {
clearTimeout(reconnectTIDRef.current);
reconnectTIDRef.current = null;
}
setIsPlaying(false);
if (wsRef.current) {
setWsState(WebSocket.CLOSED);
wsRef.current.close();
const ws = wsRef.current;
wsRef.current = null;
const currentReadyState = ws.readyState;
intentionalDisconnectRef.current = true;
setWsState(WebSocket.CLOSED);
// Remove event listeners to prevent them firing during close
try {
ws.removeEventListener("open", onOpen);
ws.removeEventListener("close", onClose);
} catch {
// Ignore errors removing listeners
}
// Only call close() if the socket is OPEN or CLOSING
// For CONNECTING or CLOSED sockets, just let it die
if (
currentReadyState === WebSocket.OPEN ||
currentReadyState === WebSocket.CLOSING
) {
try {
ws.close();
} catch {
// Ignore close errors
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bufferTimeout]);
const handlePause = useCallback(() => {
@@ -188,7 +231,14 @@ function MSEPlayer({
}
}, [isPlaying, playbackEnabled]);
const onOpen = () => {
const onOpen = useCallback(() => {
// If we were marked for intentional disconnect while connecting, close immediately
if (intentionalDisconnectRef.current) {
wsRef.current?.close();
wsRef.current = null;
return;
}
setWsState(WebSocket.OPEN);
wsRef.current?.addEventListener("message", (ev) => {
@@ -205,10 +255,27 @@ function MSEPlayer({
ondataRef.current = null;
onmessageRef.current = {};
// Reset the MSE response flag for this new connection
mseResponseReceivedRef.current = false;
// Create a fresh MediaSource for this connection to avoid stale sourceopen events
// from previous connections interfering with this one
const MediaSourceConstructor =
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
// @ts-expect-error for typing
msRef.current = new MediaSourceConstructor();
onMse();
};
// onMse is defined below and stable
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const reconnect = (timeout?: number) => {
// Don't reconnect if intentional disconnect was flagged
if (intentionalDisconnectRef.current) {
return;
}
setWsState(WebSocket.CONNECTING);
wsRef.current = null;
@@ -221,28 +288,79 @@ function MSEPlayer({
}, delay);
};
const onClose = () => {
const onClose = useCallback(() => {
// Don't reconnect if this was an intentional disconnect
if (intentionalDisconnectRef.current) {
// Reset the flag so future connects are allowed
intentionalDisconnectRef.current = false;
return;
}
if (wsState === WebSocket.CLOSED) return;
reconnect();
};
// reconnect is defined below and stable
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wsState]);
const sendWithTimeout = (value: object, timeout: number) => {
return new Promise<void>((resolve, reject) => {
// Don't start timeout if WS isn't connected - this can happen when
// sourceopen fires from a previous connection after we've already disconnected
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
// Reject so caller knows this didn't work
reject(new Error("WebSocket not connected"));
return;
}
// If we've already received an MSE response for this connection, don't start another timeout
if (mseResponseReceivedRef.current) {
resolve();
return;
}
// Clear any existing MSE timeout from a previous attempt
if (mseTimeoutRef.current !== null) {
clearTimeout(mseTimeoutRef.current);
mseTimeoutRef.current = null;
}
const timeoutId = setTimeout(() => {
reject(new Error("Timeout waiting for response"));
// Only reject if we haven't received a response yet
if (!mseResponseReceivedRef.current) {
mseTimeoutRef.current = null;
reject(new Error("Timeout waiting for response"));
}
}, timeout);
send(value);
mseTimeoutRef.current = timeoutId;
// Override the onmessageRef handler for mse type to resolve the promise on response
const originalHandler = onmessageRef.current["mse"];
onmessageRef.current["mse"] = (msg) => {
if (msg.type === "mse") {
clearTimeout(timeoutId);
if (originalHandler) originalHandler(msg);
// Mark that we've received the response
mseResponseReceivedRef.current = true;
// Clear the timeout (use ref to clear the current one, not closure)
if (mseTimeoutRef.current !== null) {
clearTimeout(mseTimeoutRef.current);
mseTimeoutRef.current = null;
}
// Call original handler in try-catch so errors don't prevent promise resolution
if (originalHandler) {
try {
originalHandler(msg);
} catch (e) {
// Don't reject - we got the response, just let the error bubble
}
}
resolve();
}
};
send(value);
});
};
@@ -292,13 +410,15 @@ function MSEPlayer({
},
(fallbackTimeout ?? 3) * 1000,
).catch(() => {
// Only report errors if we actually had a connection that failed
// If WS wasn't connected, this is a stale sourceopen event from a previous connection
if (wsRef.current) {
onDisconnect();
}
if (isIOS || isSafari) {
handleError("mse-decode", "Safari cannot open MediaSource.");
} else {
handleError("startup", "Error opening MediaSource.");
if (isIOS || isSafari) {
handleError("mse-decode", "Safari cannot open MediaSource.");
} else {
handleError("startup", "Error opening MediaSource.");
}
}
});
},
@@ -532,13 +652,6 @@ function MSEPlayer({
return;
}
// iOS 17.1+ uses ManagedMediaSource
const MediaSourceConstructor =
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
// @ts-expect-error for typing
msRef.current = new MediaSourceConstructor();
onConnect();
return () => {
@@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
onClick={() => onClick(review, false, true)}
>
{review.data.objects.sort().map((object) => {
return getIconForLabel(object, "size-3 text-white");
return getIconForLabel(
object,
"object",
"size-3 text-white",
);
})}
{review.data.audio.map((audio) => {
return getIconForLabel(audio, "size-3 text-white");
return getIconForLabel(
audio,
"audio",
"size-3 text-white",
);
})}
</Chip>
</>
@@ -417,7 +417,9 @@ export default function Step1NameCamera({
<SelectContent>
{CAMERA_BRANDS.map((brand) => (
<SelectItem key={brand.value} value={brand.value}>
{brand.label}
{brand.label.toLowerCase() === "other"
? t("label.other", { ns: "common" })
: brand.label}
</SelectItem>
))}
</SelectContent>
+27 -14
View File
@@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr";
import ActivityIndicator from "../indicators/activity-indicator";
import { Event } from "@/types/event";
import { EventType } from "@/types/search";
import { getIconForLabel } from "@/utils/iconUtil";
import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
@@ -346,22 +347,29 @@ function ReviewGroup({
: null,
);
const rawIconLabels: string[] = [
const rawIconLabels: Array<{ label: string; type: EventType }> = [
...(fetchedEvents
? fetchedEvents.map((e) =>
e.sub_label ? e.label + "-verified" : e.label,
)
: (review.data?.objects ?? [])),
...(review.data?.audio ?? []),
? fetchedEvents.map((e) => ({
label: e.sub_label ? e.label + "-verified" : e.label,
type: e.data.type,
}))
: (review.data?.objects ?? []).map((obj) => ({
label: obj,
type: "object" as EventType,
}))),
...(review.data?.audio ?? []).map((audio) => ({
label: audio,
type: "audio" as EventType,
})),
];
// limit to 5 icons
const seen = new Set<string>();
const iconLabels: string[] = [];
for (const lbl of rawIconLabels) {
if (!seen.has(lbl)) {
seen.add(lbl);
iconLabels.push(lbl);
const iconLabels: Array<{ label: string; type: EventType }> = [];
for (const item of rawIconLabels) {
if (!seen.has(item.label)) {
seen.add(item.label);
iconLabels.push(item);
if (iconLabels.length >= 5) break;
}
}
@@ -418,12 +426,12 @@ function ReviewGroup({
<div className="flex flex-row gap-3">
<div className="text-sm font-medium">{displayTime}</div>
<div className="relative flex items-center gap-2 text-white">
{iconLabels.slice(0, 5).map((lbl, idx) => (
{iconLabels.slice(0, 5).map(({ label: lbl, type }, idx) => (
<div
key={`${lbl}-${idx}`}
className="rounded-full bg-muted-foreground p-1"
>
{getIconForLabel(lbl, "size-3 text-white")}
{getIconForLabel(lbl, type, "size-3 text-white")}
</div>
))}
</div>
@@ -516,7 +524,11 @@ function ReviewGroup({
>
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
<div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")}
{getIconForLabel(
audioLabel,
"audio",
"size-3 text-white",
)}
</div>
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
</div>
@@ -618,6 +630,7 @@ function EventList({
>
{getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-3 text-white",
)}
</div>
+7 -2
View File
@@ -1,5 +1,5 @@
import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns";
import { Locale } from "date-fns/locale";
import { enUS, Locale } from "date-fns/locale";
import { formatInTimeZone } from "date-fns-tz";
import i18n from "@/utils/i18n";
export const longToDate = (long: number): Date => new Date(long * 1000);
@@ -293,9 +293,13 @@ export const getDurationFromTimestamps = (
/**
*
* @param seconds - number of seconds to convert into hours, minutes and seconds
* @param locale - the date-fns locale to use for formatting
* @returns string - formatted duration in hours, minutes and seconds
*/
export const formatSecondsToDuration = (seconds: number): string => {
export const formatSecondsToDuration = (
seconds: number,
locale?: Locale,
): string => {
if (isNaN(seconds) || seconds < 0) {
return "Invalid duration";
}
@@ -304,6 +308,7 @@ export const formatSecondsToDuration = (seconds: number): string => {
return formatDuration(duration, {
format: ["hours", "minutes", "seconds"],
delimiter: ", ",
locale: locale ?? enUS,
});
};
+24 -7
View File
@@ -1,5 +1,6 @@
import { IconName } from "@/components/icons/IconPicker";
import { FrigateConfig } from "@/types/frigateConfig";
import { EventType } from "@/types/search";
import { BsPersonWalking } from "react-icons/bs";
import {
FaAmazon,
@@ -32,6 +33,7 @@ import {
GiRabbit,
GiRaccoonHead,
GiSailboat,
GiSoundWaves,
GiSquirrel,
} from "react-icons/gi";
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
@@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is IconName {
return Object.keys(LuIcons).includes(value as IconName);
}
export function getIconForLabel(label: string, className?: string) {
export function getIconForLabel(
label: string,
type: EventType = "object",
className?: string,
) {
if (label.endsWith("-verified")) {
return getVerifiedIcon(label, className);
return getVerifiedIcon(label, className, type);
} else if (label.endsWith("-plate")) {
return getRecognizedPlateIcon(label, className);
return getRecognizedPlateIcon(label, className, type);
}
switch (label) {
@@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
case "usps":
return <FaUsps key={label} className={className} />;
default:
if (type === "audio") {
return <GiSoundWaves key={label} className={className} />;
}
return <LuLassoSelect key={label} className={className} />;
}
}
function getVerifiedIcon(label: string, className?: string) {
function getVerifiedIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)}
{getIconForLabel(simpleLabel, type, className)}
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
</div>
);
}
function getRecognizedPlateIcon(label: string, className?: string) {
function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)}
{getIconForLabel(simpleLabel, type, className)}
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
</div>
);
+4 -1
View File
@@ -12,7 +12,10 @@ export function getLifecycleItemDescription(
const label = lifecycleItem.data.sub_label
? capitalizeFirstLetter(rawLabel)
: getTranslatedLabel(rawLabel);
: getTranslatedLabel(
rawLabel,
lifecycleItem.class_type === "heard" ? "audio" : "object",
);
switch (lifecycleItem.class_type) {
case "visible":
@@ -88,7 +88,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
// title
useEffect(() => {
document.title = `${model.name} - ${t("documentTitle")}`;
document.title = `${model.name.toUpperCase()} - ${t("documentTitle")}`;
}, [model.name, t]);
// model state
@@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
: getColorForObjectName(obj.label),
}}
>
{getIconForLabel(obj.label, "size-5 text-white")}
{getIconForLabel(obj.label, "object", "size-5 text-white")}
</div>
<div className="ml-3 text-lg">
{getTranslatedLabel(obj.label)}
@@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
<div className="flex flex-row items-center gap-3 pb-1">
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
<div className="rounded-lg bg-selected p-2">
{getIconForLabel(key, "size-5 text-white")}
{getIconForLabel(key, "audio", "size-5 text-white")}
</div>
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
</div>
+1 -1
View File
@@ -855,7 +855,7 @@ export default function GeneralMetrics({
<ThresholdBarGraph
key={series.name}
graphId={`${series.name}-cpu`}
name={series.name.replaceAll("_", " ")}
name={t(`general.otherProcesses.series.${series.name}`)}
unit="%"
threshold={DetectorCpuThreshold}
updateTimes={updateTimes}