mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-21 04:26:43 +03:00
Compare commits
8 Commits
725f799cbf
...
f818fdaa3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f818fdaa3d | ||
|
|
de066d0062 | ||
|
|
f1a05d0f9b | ||
|
|
e27a94ae0b | ||
|
|
1dee548dbc | ||
|
|
91e17e12b7 | ||
|
|
bb45483e9e | ||
|
|
7b4eaf2d10 |
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -225,3 +225,29 @@ jobs:
|
|||||||
sources: |
|
sources: |
|
||||||
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
|
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
|
||||||
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi
|
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi
|
||||||
|
axera_build:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
name: AXERA Build
|
||||||
|
needs:
|
||||||
|
- amd64_build
|
||||||
|
- arm64_build
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up QEMU and Buildx
|
||||||
|
id: setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build and push Axera build
|
||||||
|
uses: docker/bake-action@v6
|
||||||
|
with:
|
||||||
|
source: .
|
||||||
|
push: true
|
||||||
|
targets: axcl
|
||||||
|
files: docker/axcl/axcl.hcl
|
||||||
|
set: |
|
||||||
|
axcl.tags=${{ steps.setup.outputs.image-name }}-axcl
|
||||||
|
*.cache-from=type=gha
|
||||||
55
docker/axcl/Dockerfile
Normal file
55
docker/axcl/Dockerfile
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# syntax=docker/dockerfile:1.6
|
||||||
|
|
||||||
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Globally set pip break-system-packages option to avoid having to specify it every time
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
|
|
||||||
|
|
||||||
|
FROM frigate AS frigate-axcl
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES
|
||||||
|
|
||||||
|
# Install axpyengine
|
||||||
|
RUN wget https://github.com/AXERA-TECH/pyaxengine/releases/download/0.1.3.rc1/axengine-0.1.3-py3-none-any.whl -O /axengine-0.1.3-py3-none-any.whl
|
||||||
|
RUN pip3 install -i https://mirrors.aliyun.com/pypi/simple/ /axengine-0.1.3-py3-none-any.whl \
|
||||||
|
&& rm /axengine-0.1.3-py3-none-any.whl
|
||||||
|
|
||||||
|
# Install axcl
|
||||||
|
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
echo "Installing x86_64 version of axcl"; \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb -O /axcl.deb; \
|
||||||
|
else \
|
||||||
|
echo "Installing aarch64 version of axcl"; \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb -O /axcl.deb; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN mkdir /unpack_axcl && \
|
||||||
|
dpkg-deb -x /axcl.deb /unpack_axcl && \
|
||||||
|
cp -R /unpack_axcl/usr/bin/axcl /usr/bin/ && \
|
||||||
|
cp -R /unpack_axcl/usr/lib/axcl /usr/lib/ && \
|
||||||
|
rm -rf /unpack_axcl /axcl.deb
|
||||||
|
|
||||||
|
|
||||||
|
# Install axcl ffmpeg
|
||||||
|
RUN mkdir -p /usr/lib/ffmpeg/axcl
|
||||||
|
|
||||||
|
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffmpeg-x64 -O /usr/lib/ffmpeg/axcl/ffmpeg && \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffprobe-x64 -O /usr/lib/ffmpeg/axcl/ffprobe; \
|
||||||
|
else \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffmpeg-aarch64 -O /usr/lib/ffmpeg/axcl/ffmpeg && \
|
||||||
|
wget https://github.com/ivanshi1108/assets/releases/download/v0.16.2/ffprobe-aarch64 -O /usr/lib/ffmpeg/axcl/ffprobe; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUN chmod +x /usr/lib/ffmpeg/axcl/ffmpeg /usr/lib/ffmpeg/axcl/ffprobe
|
||||||
|
|
||||||
|
# Set ldconfig path
|
||||||
|
RUN echo "/usr/lib/axcl" > /etc/ld.so.conf.d/ax.conf
|
||||||
|
|
||||||
|
# Set env
|
||||||
|
ENV PATH="$PATH:/usr/bin/axcl"
|
||||||
|
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/axcl"
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "ldconfig && exec /init"]
|
||||||
13
docker/axcl/axcl.hcl
Normal file
13
docker/axcl/axcl.hcl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
target frigate {
|
||||||
|
dockerfile = "docker/main/Dockerfile"
|
||||||
|
platforms = ["linux/amd64", "linux/arm64"]
|
||||||
|
target = "frigate"
|
||||||
|
}
|
||||||
|
|
||||||
|
target axcl {
|
||||||
|
dockerfile = "docker/axcl/Dockerfile"
|
||||||
|
contexts = {
|
||||||
|
frigate = "target:frigate",
|
||||||
|
}
|
||||||
|
platforms = ["linux/amd64", "linux/arm64"]
|
||||||
|
}
|
||||||
15
docker/axcl/axcl.mk
Normal file
15
docker/axcl/axcl.mk
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
BOARDS += axcl
|
||||||
|
|
||||||
|
local-axcl: version
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=frigate:latest-axcl \
|
||||||
|
--load
|
||||||
|
|
||||||
|
build-axcl: version
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-axcl
|
||||||
|
|
||||||
|
push-axcl: build-axcl
|
||||||
|
docker buildx bake --file=docker/axcl/axcl.hcl axcl \
|
||||||
|
--set axcl.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-axcl \
|
||||||
|
--push
|
||||||
83
docker/axcl/user_installation.sh
Executable file
83
docker/axcl/user_installation.sh
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Update package list and install dependencies
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential cmake git wget pciutils kmod udev
|
||||||
|
|
||||||
|
# Check if gcc-12 is needed
|
||||||
|
current_gcc_version=$(gcc --version | head -n1 | awk '{print $NF}')
|
||||||
|
gcc_major_version=$(echo $current_gcc_version | cut -d'.' -f1)
|
||||||
|
|
||||||
|
if [[ $gcc_major_version -lt 12 ]]; then
|
||||||
|
echo "Current GCC version ($current_gcc_version) is lower than 12, installing gcc-12..."
|
||||||
|
sudo apt-get install -y gcc-12
|
||||||
|
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
|
||||||
|
echo "GCC-12 installed and set as default"
|
||||||
|
else
|
||||||
|
echo "Current GCC version ($current_gcc_version) is sufficient, skipping GCC installation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine architecture
|
||||||
|
arch=$(uname -m)
|
||||||
|
download_url=""
|
||||||
|
|
||||||
|
if [[ $arch == "x86_64" ]]; then
|
||||||
|
download_url="https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
deb_file="axcl_host_x86_64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
elif [[ $arch == "aarch64" ]]; then
|
||||||
|
download_url="https://github.com/ivanshi1108/assets/releases/download/v0.16.2/axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
deb_file="axcl_host_aarch64_V3.6.5_20250908154509_NO4973.deb"
|
||||||
|
else
|
||||||
|
echo "Unsupported architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download AXCL driver
|
||||||
|
echo "Downloading AXCL driver for $arch..."
|
||||||
|
wget "$download_url" -O "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to download AXCL driver"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install AXCL driver
|
||||||
|
echo "Installing AXCL driver..."
|
||||||
|
sudo dpkg -i "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to install AXCL driver, attempting to fix dependencies..."
|
||||||
|
sudo apt-get install -f -y
|
||||||
|
sudo dpkg -i "$deb_file"
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "AXCL driver installation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update environment
|
||||||
|
echo "Updating environment..."
|
||||||
|
source /etc/profile
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
echo "Verifying AXCL installation..."
|
||||||
|
if command -v axcl-smi &> /dev/null; then
|
||||||
|
echo "AXCL driver detected, checking AI accelerator status..."
|
||||||
|
|
||||||
|
axcl_output=$(axcl-smi 2>&1)
|
||||||
|
axcl_exit_code=$?
|
||||||
|
|
||||||
|
echo "$axcl_output"
|
||||||
|
|
||||||
|
if [ $axcl_exit_code -eq 0 ]; then
|
||||||
|
echo "AXCL driver installation completed successfully!"
|
||||||
|
else
|
||||||
|
echo "AXCL driver installed but no AI accelerator detected or communication failed."
|
||||||
|
echo "Please check if the AI accelerator is properly connected and powered on."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "axcl-smi command not found. AXCL driver installation may have failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@ -47,6 +47,11 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
||||||
|
|
||||||
|
**AXERA**
|
||||||
|
|
||||||
|
- [AXEngine](#axera): axmodels can run on AXERA AI acceleration.
|
||||||
|
|
||||||
|
|
||||||
**For Testing**
|
**For Testing**
|
||||||
|
|
||||||
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
||||||
@ -1169,6 +1174,41 @@ model: # required
|
|||||||
labelmap_path: /labelmap/coco-80.txt # required
|
labelmap_path: /labelmap/coco-80.txt # required
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## AXERA
|
||||||
|
|
||||||
|
Hardware accelerated object detection is supported on the following SoCs:
|
||||||
|
|
||||||
|
- AX650N
|
||||||
|
- AX8850N
|
||||||
|
|
||||||
|
This implementation uses the [AXera Pulsar2 Toolchain](https://huggingface.co/AXERA-TECH/Pulsar2).
|
||||||
|
|
||||||
|
See the [installation docs](../frigate/installation.md#axera) for information on configuring the AXEngine hardware.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
When configuring the AXEngine detector, you have to specify the model name.
|
||||||
|
|
||||||
|
#### yolov9
|
||||||
|
|
||||||
|
A yolov9 model is provided in the container at /axmodels and is used by this detector type by default.
|
||||||
|
|
||||||
|
Use the model configuration shown below when using the axengine detector with the default axmodel:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
detectors: # required
|
||||||
|
axengine: # required
|
||||||
|
type: axengine # required
|
||||||
|
|
||||||
|
model: # required
|
||||||
|
path: frigate-yolov9-tiny # required
|
||||||
|
model_type: yolo-generic # required
|
||||||
|
width: 320 # required
|
||||||
|
height: 320 # required
|
||||||
|
tensor_format: bgr # required
|
||||||
|
labelmap_path: /labelmap/coco-80.txt # required
|
||||||
|
```
|
||||||
|
|
||||||
## Rockchip platform
|
## Rockchip platform
|
||||||
|
|
||||||
Hardware accelerated object detection is supported on the following SoCs:
|
Hardware accelerated object detection is supported on the following SoCs:
|
||||||
|
|||||||
@ -110,6 +110,14 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
| ssd mobilenet | ~ 25 ms |
|
| ssd mobilenet | ~ 25 ms |
|
||||||
| yolov5m | ~ 118 ms |
|
| yolov5m | ~ 118 ms |
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
- **AXEngine** Default model is **yolov9**
|
||||||
|
|
||||||
|
| Name | AXERA AX650N/AX8850N Inference Time |
|
||||||
|
| ---------------- | ----------------------------------- |
|
||||||
|
| yolov9-tiny | ~ 1.012 ms |
|
||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
|
|
||||||
Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
|
Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
|
||||||
|
|||||||
@ -287,6 +287,40 @@ or add these options to your `docker run` command:
|
|||||||
|
|
||||||
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
AXERA accelerators are available in an M.2 form factor, compatible with both Raspberry Pi and Orange Pi. This form factor has also been successfully tested on x86 platforms, making it a versatile choice for various computing environments.
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
Using AXERA accelerators requires the installation of the AXCL driver. We provide a convenient Linux script to complete this installation.
|
||||||
|
|
||||||
|
Follow these steps for installation:
|
||||||
|
|
||||||
|
1. Copy or download [this script](https://github.com/ivanshi1108/assets/releases/download/v0.16.2/user_installation.sh).
|
||||||
|
2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh`
|
||||||
|
3. Run the script with `./user_installation.sh`
|
||||||
|
|
||||||
|
#### Setup
|
||||||
|
|
||||||
|
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
|
||||||
|
|
||||||
|
Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
devices:
|
||||||
|
- /dev/axcl_host
|
||||||
|
- /dev/ax_mmb_dev
|
||||||
|
- /dev/msg_userdev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using `docker run`, add this option to your command `--device /dev/axcl_host --device /dev/ax_mmb_dev --device /dev/msg_userdev`
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
Finally, configure [hardware object detection](/configuration/object_detectors#axera) to complete the setup.
|
||||||
|
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Running through Docker with Docker Compose is the recommended install method.
|
Running through Docker with Docker Compose is the recommended install method.
|
||||||
|
|||||||
@ -255,6 +255,7 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
||||||
self.model_path = model_path
|
self.model_path = model_path
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.model_type = model_type
|
||||||
|
|
||||||
if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported(
|
if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported(
|
||||||
model_type
|
model_type
|
||||||
@ -341,6 +342,13 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
# Lock prevents concurrent access to infer_request
|
# Lock prevents concurrent access to infer_request
|
||||||
# Needed for JinaV2: genai thread (text) + embeddings thread (vision)
|
# Needed for JinaV2: genai thread (text) + embeddings thread (vision)
|
||||||
with self._inference_lock:
|
with self._inference_lock:
|
||||||
|
from frigate.embeddings.types import EnrichmentModelTypeEnum
|
||||||
|
|
||||||
|
if self.model_type in [EnrichmentModelTypeEnum.arcface.value]:
|
||||||
|
# For face recognition models, create a fresh infer_request
|
||||||
|
# for each inference to avoid state pollution that causes incorrect results.
|
||||||
|
self.infer_request = self.compiled_model.create_infer_request()
|
||||||
|
|
||||||
# Handle single input case for backward compatibility
|
# Handle single input case for backward compatibility
|
||||||
if (
|
if (
|
||||||
len(inputs) == 1
|
len(inputs) == 1
|
||||||
|
|||||||
92
frigate/detectors/plugins/axengine.py
Normal file
92
frigate/detectors/plugins/axengine.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
|
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||||
|
from frigate.util.model import post_process_yolo
|
||||||
|
|
||||||
|
import axengine as axe
|
||||||
|
from axengine import axclrt_provider_name, axengine_provider_name
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DETECTOR_KEY = "axengine"
|
||||||
|
|
||||||
|
supported_models = {
|
||||||
|
ModelTypeEnum.yologeneric: "frigate-yolov9-.*$",
|
||||||
|
}
|
||||||
|
|
||||||
|
model_cache_dir = os.path.join(MODEL_CACHE_DIR, "axengine_cache/")
|
||||||
|
|
||||||
|
|
||||||
|
class AxengineDetectorConfig(BaseDetectorConfig):
|
||||||
|
type: Literal[DETECTOR_KEY]
|
||||||
|
|
||||||
|
class Axengine(DetectionApi):
|
||||||
|
type_key = DETECTOR_KEY
|
||||||
|
def __init__(self, config: AxengineDetectorConfig):
|
||||||
|
logger.info("__init__ axengine")
|
||||||
|
super().__init__(config)
|
||||||
|
self.height = config.model.height
|
||||||
|
self.width = config.model.width
|
||||||
|
model_path = config.model.path or "frigate-yolov9-tiny"
|
||||||
|
model_props = self.parse_model_input(model_path)
|
||||||
|
self.session = axe.InferenceSession(model_props["path"])
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_model_input(self, model_path):
|
||||||
|
model_props = {}
|
||||||
|
model_props["preset"] = True
|
||||||
|
|
||||||
|
model_matched = False
|
||||||
|
|
||||||
|
for model_type, pattern in supported_models.items():
|
||||||
|
if re.match(pattern, model_path):
|
||||||
|
model_matched = True
|
||||||
|
model_props["model_type"] = model_type
|
||||||
|
|
||||||
|
if model_matched:
|
||||||
|
model_props["filename"] = model_path + f".axmodel"
|
||||||
|
model_props["path"] = model_cache_dir + model_props["filename"]
|
||||||
|
|
||||||
|
if not os.path.isfile(model_props["path"]):
|
||||||
|
self.download_model(model_props["filename"])
|
||||||
|
else:
|
||||||
|
supported_models_str = ", ".join(
|
||||||
|
model[1:-1] for model in supported_models
|
||||||
|
)
|
||||||
|
raise Exception(
|
||||||
|
f"Model {model_path} is unsupported. Provide your own model or choose one of the following: {supported_models_str}"
|
||||||
|
)
|
||||||
|
return model_props
|
||||||
|
|
||||||
|
def download_model(self, filename):
|
||||||
|
if not os.path.isdir(model_cache_dir):
|
||||||
|
os.mkdir(model_cache_dir)
|
||||||
|
|
||||||
|
GITHUB_ENDPOINT = os.environ.get("GITHUB_ENDPOINT", "https://github.com")
|
||||||
|
urllib.request.urlretrieve(
|
||||||
|
f"{GITHUB_ENDPOINT}/ivanshi1108/assets/releases/download/v0.16.2/{filename}",
|
||||||
|
model_cache_dir + filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_raw(self, tensor_input):
|
||||||
|
results = None
|
||||||
|
results = self.session.run(None, {"images": tensor_input})
|
||||||
|
if self.detector_config.model.model_type == ModelTypeEnum.yologeneric:
|
||||||
|
return post_process_yolo(results, self.width, self.height)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f'Model type "{self.detector_config.model.model_type}" is currently not supported.'
|
||||||
|
)
|
||||||
|
|
||||||
@ -72,7 +72,10 @@
|
|||||||
"formattedTimestampFilename": {
|
"formattedTimestampFilename": {
|
||||||
"12hour": "MM-dd-yy-h-mm-ss-a",
|
"12hour": "MM-dd-yy-h-mm-ss-a",
|
||||||
"24hour": "MM-dd-yy-HH-mm-ss"
|
"24hour": "MM-dd-yy-HH-mm-ss"
|
||||||
}
|
},
|
||||||
|
"inProgress": "In progress",
|
||||||
|
"invalidStartTime": "Invalid start time",
|
||||||
|
"invalidEndTime": "Invalid end time"
|
||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"speed": {
|
"speed": {
|
||||||
@ -144,7 +147,8 @@
|
|||||||
"unselect": "Unselect",
|
"unselect": "Unselect",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"deleteNow": "Delete Now",
|
"deleteNow": "Delete Now",
|
||||||
"next": "Next"
|
"next": "Next",
|
||||||
|
"continue": "Continue"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"system": "System",
|
"system": "System",
|
||||||
@ -237,6 +241,7 @@
|
|||||||
"export": "Export",
|
"export": "Export",
|
||||||
"uiPlayground": "UI Playground",
|
"uiPlayground": "UI Playground",
|
||||||
"faceLibrary": "Face Library",
|
"faceLibrary": "Face Library",
|
||||||
|
"classification": "Classification",
|
||||||
"user": {
|
"user": {
|
||||||
"title": "User",
|
"title": "User",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
"label": "Detail",
|
"label": "Detail",
|
||||||
"noDataFound": "No detail data to review",
|
"noDataFound": "No detail data to review",
|
||||||
"aria": "Toggle detail view",
|
"aria": "Toggle detail view",
|
||||||
"trackedObject_one": "object",
|
"trackedObject_one": "{{count}} object",
|
||||||
"trackedObject_other": "objects",
|
"trackedObject_other": "{{count}} objects",
|
||||||
"noObjectDetailData": "No object detail data available.",
|
"noObjectDetailData": "No object detail data available.",
|
||||||
"settings": "Detail View Settings",
|
"settings": "Detail View Settings",
|
||||||
"alwaysExpandActive": {
|
"alwaysExpandActive": {
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
"snapshot": "snapshot",
|
"snapshot": "snapshot",
|
||||||
"thumbnail": "thumbnail",
|
"thumbnail": "thumbnail",
|
||||||
"video": "video",
|
"video": "video",
|
||||||
"object_lifecycle": "object lifecycle"
|
"tracking_details": "tracking details"
|
||||||
},
|
},
|
||||||
"trackingDetails": {
|
"trackingDetails": {
|
||||||
"title": "Tracking Details",
|
"title": "Tracking Details",
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"masksAndZones": "Mask and Zone Editor - Frigate",
|
"masksAndZones": "Mask and Zone Editor - Frigate",
|
||||||
"motionTuner": "Motion Tuner - Frigate",
|
"motionTuner": "Motion Tuner - Frigate",
|
||||||
"object": "Debug - Frigate",
|
"object": "Debug - Frigate",
|
||||||
"general": "General Settings - Frigate",
|
"general": "UI Settings - Frigate",
|
||||||
"frigatePlus": "Frigate+ Settings - Frigate",
|
"frigatePlus": "Frigate+ Settings - Frigate",
|
||||||
"notifications": "Notification Settings - Frigate"
|
"notifications": "Notification Settings - Frigate"
|
||||||
},
|
},
|
||||||
@ -37,7 +37,7 @@
|
|||||||
"noCamera": "No Camera"
|
"noCamera": "No Camera"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"title": "General Settings",
|
"title": "UI Settings",
|
||||||
"liveDashboard": {
|
"liveDashboard": {
|
||||||
"title": "Live Dashboard",
|
"title": "Live Dashboard",
|
||||||
"automaticLiveView": {
|
"automaticLiveView": {
|
||||||
@ -51,6 +51,10 @@
|
|||||||
"displayCameraNames": {
|
"displayCameraNames": {
|
||||||
"label": "Always Show Camera Names",
|
"label": "Always Show Camera Names",
|
||||||
"desc": "Always show the camera names in a chip in the multi-camera live view dashboard."
|
"desc": "Always show the camera names in a chip in the multi-camera live view dashboard."
|
||||||
|
},
|
||||||
|
"liveFallbackTimeout": {
|
||||||
|
"label": "Live Player Fallback Timeout",
|
||||||
|
"desc": "When a camera's high quality live stream is unavailable, fall back to low bandwidth mode after this many seconds. Default: 3."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storedLayouts": {
|
"storedLayouts": {
|
||||||
|
|||||||
@ -454,6 +454,24 @@ export function GeneralFilterContent({
|
|||||||
onClose,
|
onClose,
|
||||||
}: GeneralFilterContentProps) {
|
}: GeneralFilterContentProps) {
|
||||||
const { t } = useTranslation(["components/filter", "views/events"]);
|
const { t } = useTranslation(["components/filter", "views/events"]);
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
});
|
||||||
|
const allAudioListenLabels = useMemo<string[]>(() => {
|
||||||
|
if (!config) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = new Set<string>();
|
||||||
|
Object.values(config.cameras).forEach((camera) => {
|
||||||
|
if (camera?.audio?.enabled) {
|
||||||
|
camera.audio.listen.forEach((label) => {
|
||||||
|
labels.add(label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...labels].sort();
|
||||||
|
}, [config]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||||
@ -504,7 +522,10 @@ export function GeneralFilterContent({
|
|||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
label={getTranslatedLabel(item)}
|
label={getTranslatedLabel(
|
||||||
|
item,
|
||||||
|
allAudioListenLabels.includes(item) ? "audio" : "object",
|
||||||
|
)}
|
||||||
isChecked={filter.labels?.includes(item) ?? false}
|
isChecked={filter.labels?.includes(item) ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
|
|||||||
@ -81,6 +81,43 @@ export default function InputWithTags({
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allAudioListenLabels = useMemo<Set<string>>(() => {
|
||||||
|
if (!config) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = new Set<string>();
|
||||||
|
Object.values(config.cameras).forEach((camera) => {
|
||||||
|
if (camera?.audio?.enabled) {
|
||||||
|
camera.audio.listen.forEach((label) => {
|
||||||
|
labels.add(label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return labels;
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
const translatedAudioLabelMap = useMemo<Map<string, string>>(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
if (!config) return map;
|
||||||
|
|
||||||
|
allAudioListenLabels.forEach((label) => {
|
||||||
|
// getTranslatedLabel likely depends on i18n internally; including `lang`
|
||||||
|
// in deps ensures this map is rebuilt when language changes
|
||||||
|
map.set(label, getTranslatedLabel(label, "audio"));
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [allAudioListenLabels, config]);
|
||||||
|
|
||||||
|
function resolveLabel(value: string) {
|
||||||
|
const mapped = translatedAudioLabelMap.get(value);
|
||||||
|
if (mapped) return mapped;
|
||||||
|
return getTranslatedLabel(
|
||||||
|
value,
|
||||||
|
allAudioListenLabels.has(value) ? "audio" : "object",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState(search || "");
|
const [inputValue, setInputValue] = useState(search || "");
|
||||||
const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
|
const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
|
||||||
null,
|
null,
|
||||||
@ -421,7 +458,8 @@ export default function InputWithTags({
|
|||||||
? t("button.yes", { ns: "common" })
|
? t("button.yes", { ns: "common" })
|
||||||
: t("button.no", { ns: "common" });
|
: t("button.no", { ns: "common" });
|
||||||
} else if (filterType === "labels") {
|
} else if (filterType === "labels") {
|
||||||
return getTranslatedLabel(String(filterValues));
|
const value = String(filterValues);
|
||||||
|
return resolveLabel(value);
|
||||||
} else if (filterType === "search_type") {
|
} else if (filterType === "search_type") {
|
||||||
return t("filter.searchType." + String(filterValues));
|
return t("filter.searchType." + String(filterValues));
|
||||||
} else {
|
} else {
|
||||||
@ -828,7 +866,7 @@ export default function InputWithTags({
|
|||||||
>
|
>
|
||||||
{t("filter.label." + filterType)}:{" "}
|
{t("filter.label." + filterType)}:{" "}
|
||||||
{filterType === "labels" ? (
|
{filterType === "labels" ? (
|
||||||
getTranslatedLabel(value)
|
resolveLabel(value)
|
||||||
) : filterType === "cameras" ? (
|
) : filterType === "cameras" ? (
|
||||||
<CameraNameLabel camera={value} />
|
<CameraNameLabel camera={value} />
|
||||||
) : filterType === "zones" ? (
|
) : filterType === "zones" ? (
|
||||||
|
|||||||
@ -1155,7 +1155,7 @@ function ObjectDetailsTab({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
|
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
|
||||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||||
{getTranslatedLabel(search.label)}
|
{getTranslatedLabel(search.label, search.data.type)}
|
||||||
{search.sub_label && ` (${search.sub_label})`}
|
{search.sub_label && ` (${search.sub_label})`}
|
||||||
{isAdmin && search.end_time && (
|
{isAdmin && search.end_time && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -1394,7 +1394,9 @@ function ObjectDetailsTab({
|
|||||||
{state == "submitted" && (
|
{state == "submitted" && (
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
<FaCheckCircle className="size-4 text-success" />
|
<FaCheckCircle className="size-4 text-success" />
|
||||||
{t("explore.plus.review.state.submitted")}
|
{t("explore.plus.review.state.submitted", {
|
||||||
|
ns: "components/dialog",
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -343,6 +343,10 @@ export function TrackingDetails({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [displayedRecordTime]);
|
}, [displayedRecordTime]);
|
||||||
|
|
||||||
|
const onUploadFrameToPlus = useCallback(() => {
|
||||||
|
return axios.post(`/${event.camera}/plus/${currentTime}`);
|
||||||
|
}, [event.camera, currentTime]);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -388,6 +392,7 @@ export function TrackingDetails({
|
|||||||
frigateControls={true}
|
frigateControls={true}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
onSeekToTime={handleSeekToTime}
|
onSeekToTime={handleSeekToTime}
|
||||||
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
isDetailMode={true}
|
isDetailMode={true}
|
||||||
camera={event.camera}
|
camera={event.camera}
|
||||||
currentTimeOverride={currentTime}
|
currentTimeOverride={currentTime}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import {
|
import {
|
||||||
LivePlayerError,
|
LivePlayerError,
|
||||||
PlayerStatsType,
|
PlayerStatsType,
|
||||||
@ -71,6 +72,8 @@ function MSEPlayer({
|
|||||||
const [errorCount, setErrorCount] = useState<number>(0);
|
const [errorCount, setErrorCount] = useState<number>(0);
|
||||||
const totalBytesLoaded = useRef(0);
|
const totalBytesLoaded = useRef(0);
|
||||||
|
|
||||||
|
const [fallbackTimeout] = usePersistence<number>("liveFallbackTimeout", 3);
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTIDRef = useRef<number | null>(null);
|
const reconnectTIDRef = useRef<number | null>(null);
|
||||||
@ -475,7 +478,10 @@ function MSEPlayer({
|
|||||||
setBufferTimeout(undefined);
|
setBufferTimeout(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutDuration = bufferTime == 0 ? 5000 : 3000;
|
const timeoutDuration =
|
||||||
|
bufferTime == 0
|
||||||
|
? (fallbackTimeout ?? 3) * 2 * 1000
|
||||||
|
: (fallbackTimeout ?? 3) * 1000;
|
||||||
setBufferTimeout(
|
setBufferTimeout(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (
|
if (
|
||||||
@ -500,6 +506,7 @@ function MSEPlayer({
|
|||||||
onError,
|
onError,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
playbackEnabled,
|
playbackEnabled,
|
||||||
|
fallbackTimeout,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -349,7 +349,7 @@ function ReviewGroup({
|
|||||||
? fetchedEvents.length
|
? fetchedEvents.length
|
||||||
: (review.data.objects ?? []).length;
|
: (review.data.objects ?? []).length;
|
||||||
|
|
||||||
return `${objectCount} ${t("detail.trackedObject", { count: objectCount })}`;
|
return `${t("detail.trackedObject", { count: objectCount })}`;
|
||||||
}, [review, t, fetchedEvents]);
|
}, [review, t, fetchedEvents]);
|
||||||
|
|
||||||
const reviewDuration = useMemo(
|
const reviewDuration = useMemo(
|
||||||
@ -478,7 +478,7 @@ function ReviewGroup({
|
|||||||
<div className="rounded-full bg-muted-foreground p-1">
|
<div className="rounded-full bg-muted-foreground p-1">
|
||||||
{getIconForLabel(audioLabel, "size-3 text-white")}
|
{getIconForLabel(audioLabel, "size-3 text-white")}
|
||||||
</div>
|
</div>
|
||||||
<span>{getTranslatedLabel(audioLabel)}</span>
|
<span>{getTranslatedLabel(audioLabel, "audio")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -513,7 +513,8 @@ function EventList({
|
|||||||
|
|
||||||
const isSelected = selectedObjectIds.includes(event.id);
|
const isSelected = selectedObjectIds.includes(event.id);
|
||||||
|
|
||||||
const label = event.sub_label || getTranslatedLabel(event.label);
|
const label =
|
||||||
|
event.sub_label || getTranslatedLabel(event.label, event.data.type);
|
||||||
|
|
||||||
const handleObjectSelect = (event: Event | undefined) => {
|
const handleObjectSelect = (event: Event | undefined) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { LivePlayerMode, LiveStreamMetadata } from "@/types/live";
|
|||||||
export default function useCameraLiveMode(
|
export default function useCameraLiveMode(
|
||||||
cameras: CameraConfig[],
|
cameras: CameraConfig[],
|
||||||
windowVisible: boolean,
|
windowVisible: boolean,
|
||||||
|
activeStreams?: { [cameraName: string]: string },
|
||||||
) {
|
) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -20,16 +21,20 @@ export default function useCameraLiveMode(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isRestreamed) {
|
if (isRestreamed) {
|
||||||
|
if (activeStreams && activeStreams[camera.name]) {
|
||||||
|
streamNames.add(activeStreams[camera.name]);
|
||||||
|
} else {
|
||||||
Object.values(camera.live.streams).forEach((streamName) => {
|
Object.values(camera.live.streams).forEach((streamName) => {
|
||||||
streamNames.add(streamName);
|
streamNames.add(streamName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streamNames.size > 0
|
return streamNames.size > 0
|
||||||
? Array.from(streamNames).sort().join(",")
|
? Array.from(streamNames).sort().join(",")
|
||||||
: null;
|
: null;
|
||||||
}, [cameras, config]);
|
}, [cameras, config, activeStreams]);
|
||||||
|
|
||||||
const streamsFetcher = useCallback(async (key: string) => {
|
const streamsFetcher = useCallback(async (key: string) => {
|
||||||
const streamNames = key.split(",");
|
const streamNames = key.split(",");
|
||||||
@ -68,7 +73,9 @@ export default function useCameraLiveMode(
|
|||||||
[key: string]: LiveStreamMetadata;
|
[key: string]: LiveStreamMetadata;
|
||||||
}>(restreamedStreamsKey, streamsFetcher, {
|
}>(restreamedStreamsKey, streamsFetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
dedupingInterval: 10000,
|
revalidateOnReconnect: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
||||||
|
|||||||
@ -244,12 +244,12 @@ export const getDurationFromTimestamps = (
|
|||||||
abbreviated: boolean = false,
|
abbreviated: boolean = false,
|
||||||
): string => {
|
): string => {
|
||||||
if (isNaN(start_time)) {
|
if (isNaN(start_time)) {
|
||||||
return "Invalid start time";
|
return i18n.t("time.invalidStartTime", { ns: "common" });
|
||||||
}
|
}
|
||||||
let duration = "In Progress";
|
let duration = i18n.t("time.inProgress", { ns: "common" });
|
||||||
if (end_time !== null) {
|
if (end_time !== null) {
|
||||||
if (isNaN(end_time)) {
|
if (isNaN(end_time)) {
|
||||||
return "Invalid end time";
|
return i18n.t("time.invalidEndTime", { ns: "common" });
|
||||||
}
|
}
|
||||||
const start = fromUnixTime(start_time);
|
const start = fromUnixTime(start_time);
|
||||||
const end = fromUnixTime(end_time);
|
const end = fromUnixTime(end_time);
|
||||||
|
|||||||
@ -86,14 +86,6 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
// preferred live modes per camera
|
// preferred live modes per camera
|
||||||
|
|
||||||
const {
|
|
||||||
preferredLiveModes,
|
|
||||||
setPreferredLiveModes,
|
|
||||||
resetPreferredLiveMode,
|
|
||||||
isRestreamedStates,
|
|
||||||
supportsAudioOutputStates,
|
|
||||||
} = useCameraLiveMode(cameras, windowVisible);
|
|
||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||||
|
|
||||||
@ -106,6 +98,33 @@ export default function DraggableGridLayout({
|
|||||||
}
|
}
|
||||||
}, [allGroupsStreamingSettings, cameraGroup]);
|
}, [allGroupsStreamingSettings, cameraGroup]);
|
||||||
|
|
||||||
|
const activeStreams = useMemo(() => {
|
||||||
|
const streams: { [cameraName: string]: string } = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
const availableStreams = camera.live.streams || {};
|
||||||
|
const streamNameFromSettings =
|
||||||
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
||||||
|
const streamExists =
|
||||||
|
streamNameFromSettings &&
|
||||||
|
Object.values(availableStreams).includes(streamNameFromSettings);
|
||||||
|
|
||||||
|
const streamName = streamExists
|
||||||
|
? streamNameFromSettings
|
||||||
|
: Object.values(availableStreams)[0] || "";
|
||||||
|
|
||||||
|
streams[camera.name] = streamName;
|
||||||
|
});
|
||||||
|
return streams;
|
||||||
|
}, [cameras, currentGroupStreamingSettings]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
||||||
|
|
||||||
// grid layout
|
// grid layout
|
||||||
|
|
||||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||||
|
|||||||
@ -162,6 +162,9 @@ export default function LiveCameraView({
|
|||||||
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1027,6 +1030,7 @@ function FrigateCameraFeatures({
|
|||||||
disabled={!cameraEnabled || debug || isSnapshotLoading}
|
disabled={!cameraEnabled || debug || isSnapshotLoading}
|
||||||
loading={isSnapshotLoading}
|
loading={isSnapshotLoading}
|
||||||
/>
|
/>
|
||||||
|
{!fullscreen && (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<div
|
<div
|
||||||
@ -1155,7 +1159,9 @@ function FrigateCameraFeatures({
|
|||||||
{t("stream.audio.tips.title")}
|
{t("stream.audio.tips.title")}
|
||||||
<div className="mt-2 flex items-center text-primary">
|
<div className="mt-2 flex items-center text-primary">
|
||||||
<Link
|
<Link
|
||||||
to={getLocaleDocUrl("configuration/live")}
|
to={getLocaleDocUrl(
|
||||||
|
"configuration/live",
|
||||||
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline"
|
className="inline"
|
||||||
@ -1315,6 +1321,7 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,14 +202,6 @@ export default function LiveDashboardView({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {
|
|
||||||
preferredLiveModes,
|
|
||||||
setPreferredLiveModes,
|
|
||||||
resetPreferredLiveMode,
|
|
||||||
isRestreamedStates,
|
|
||||||
supportsAudioOutputStates,
|
|
||||||
} = useCameraLiveMode(cameras, windowVisible);
|
|
||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||||
|
|
||||||
@ -239,6 +231,33 @@ export default function LiveDashboardView({
|
|||||||
[visibleCameraObserver.current],
|
[visibleCameraObserver.current],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeStreams = useMemo(() => {
|
||||||
|
const streams: { [cameraName: string]: string } = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
const availableStreams = camera.live.streams || {};
|
||||||
|
const streamNameFromSettings =
|
||||||
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
||||||
|
const streamExists =
|
||||||
|
streamNameFromSettings &&
|
||||||
|
Object.values(availableStreams).includes(streamNameFromSettings);
|
||||||
|
|
||||||
|
const streamName = streamExists
|
||||||
|
? streamNameFromSettings
|
||||||
|
: Object.values(availableStreams)[0] || "";
|
||||||
|
|
||||||
|
streams[camera.name] = streamName;
|
||||||
|
});
|
||||||
|
return streams;
|
||||||
|
}, [cameras, currentGroupStreamingSettings]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
||||||
|
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
|
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
|
|||||||
@ -649,7 +649,7 @@ export function RecordingView({
|
|||||||
value="detail"
|
value="detail"
|
||||||
aria-label="Detail Stream"
|
aria-label="Detail Stream"
|
||||||
>
|
>
|
||||||
<div className="">Detail</div>
|
<div className="">{t("detail.label")}</div>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -99,6 +99,10 @@ export default function UiSettingsView() {
|
|||||||
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
||||||
const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0);
|
const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0);
|
||||||
const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true);
|
const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true);
|
||||||
|
const [fallbackTimeout, setFallbackTimeout] = usePersistence(
|
||||||
|
"liveFallbackTimeout",
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -161,6 +165,48 @@ export default function UiSettingsView() {
|
|||||||
<p>{t("general.liveDashboard.displayCameraNames.desc")}</p>
|
<p>{t("general.liveDashboard.displayCameraNames.desc")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-row items-center justify-start gap-2">
|
||||||
|
<Label
|
||||||
|
className="cursor-pointer"
|
||||||
|
htmlFor="live-fallback-timeout"
|
||||||
|
>
|
||||||
|
{t("general.liveDashboard.liveFallbackTimeout.label")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||||
|
<p>{t("general.liveDashboard.liveFallbackTimeout.desc")}</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={fallbackTimeout?.toString()}
|
||||||
|
onValueChange={(value) => setFallbackTimeout(parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
{t("time.second", {
|
||||||
|
ns: "common",
|
||||||
|
time: fallbackTimeout,
|
||||||
|
count: fallbackTimeout,
|
||||||
|
})}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((timeout) => (
|
||||||
|
<SelectItem
|
||||||
|
key={timeout}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={timeout.toString()}
|
||||||
|
>
|
||||||
|
{t("time.second", {
|
||||||
|
ns: "common",
|
||||||
|
time: timeout,
|
||||||
|
count: timeout,
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-3 flex w-full flex-col space-y-6">
|
<div className="my-3 flex w-full flex-col space-y-6">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user