mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
Compare commits
No commits in common. "192aba901ab3164ddf05b2021d929cf0b2b08e13" and "a0b82715324dfe34a999c4b57daa31650f8a1752" have entirely different histories.
192aba901a
...
a0b8271532
@ -266,12 +266,6 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
|||||||
RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \
|
RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \
|
||||||
pip3 install -U /deps/wheels/*.whl
|
pip3 install -U /deps/wheels/*.whl
|
||||||
|
|
||||||
# Install Axera Engine
|
|
||||||
RUN pip3 install https://github.com/AXERA-TECH/pyaxengine/releases/download/0.1.3-frigate/axengine-0.1.3-py3-none-any.whl
|
|
||||||
|
|
||||||
ENV PATH="${PATH}:/usr/bin/axcl"
|
|
||||||
ENV LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/usr/lib/axcl"
|
|
||||||
|
|
||||||
# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image)
|
# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image)
|
||||||
RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \
|
RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \
|
||||||
bash -c "bash /deps/install_memryx.sh"
|
bash -c "bash /deps/install_memryx.sh"
|
||||||
|
|||||||
@ -73,7 +73,6 @@ cd /tmp/nginx
|
|||||||
--with-file-aio \
|
--with-file-aio \
|
||||||
--with-http_sub_module \
|
--with-http_sub_module \
|
||||||
--with-http_ssl_module \
|
--with-http_ssl_module \
|
||||||
--with-http_v2_module \
|
|
||||||
--with-http_auth_request_module \
|
--with-http_auth_request_module \
|
||||||
--with-http_realip_module \
|
--with-http_realip_module \
|
||||||
--with-threads \
|
--with-threads \
|
||||||
|
|||||||
@ -63,9 +63,6 @@ http {
|
|||||||
server {
|
server {
|
||||||
include listen.conf;
|
include listen.conf;
|
||||||
|
|
||||||
# enable HTTP/2 for TLS connections to eliminate browser 6-connection limit
|
|
||||||
http2 on;
|
|
||||||
|
|
||||||
# vod settings
|
# vod settings
|
||||||
vod_base_url '';
|
vod_base_url '';
|
||||||
vod_segments_base_url '';
|
vod_segments_base_url '';
|
||||||
|
|||||||
@ -49,11 +49,6 @@ 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** <CommunityBadge />
|
|
||||||
|
|
||||||
- [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.
|
||||||
@ -1483,41 +1478,6 @@ model:
|
|||||||
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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:
|
|
||||||
axengine:
|
|
||||||
type: axengine
|
|
||||||
|
|
||||||
model:
|
|
||||||
path: frigate-yolov9-tiny
|
|
||||||
model_type: yolo-generic
|
|
||||||
width: 320
|
|
||||||
height: 320
|
|
||||||
tensor_format: bgr
|
|
||||||
labelmap_path: /labelmap/coco-80.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
|
|
||||||
Some model types are not included in Frigate by default.
|
Some model types are not included in Frigate by default.
|
||||||
@ -1611,12 +1571,12 @@ YOLOv9 model can be exported as ONNX using the command below. You can copy and p
|
|||||||
```sh
|
```sh
|
||||||
docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
|
docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
|
||||||
FROM python:3.11 AS build
|
FROM python:3.11 AS build
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
|
||||||
WORKDIR /yolov9
|
WORKDIR /yolov9
|
||||||
ADD https://github.com/WongKinYiu/yolov9.git .
|
ADD https://github.com/WongKinYiu/yolov9.git .
|
||||||
RUN uv pip install --system -r requirements.txt
|
RUN uv pip install --system -r requirements.txt
|
||||||
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
|
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 onnxscript
|
||||||
ARG MODEL_SIZE
|
ARG MODEL_SIZE
|
||||||
ARG IMG_SIZE
|
ARG IMG_SIZE
|
||||||
ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
|
ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
|
||||||
|
|||||||
@ -103,10 +103,6 @@ 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 to provide efficient object detection.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection.
|
||||||
|
|
||||||
**AXERA** <CommunityBadge />
|
|
||||||
|
|
||||||
- [AXEngine](#axera): axera models can run on AXERA NPUs via AXEngine, delivering highly efficient object detection.
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
@ -292,14 +288,6 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
|
|||||||
| 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 | ~ 4 ms |
|
|
||||||
|
|
||||||
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
||||||
|
|
||||||
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
||||||
|
|||||||
@ -439,39 +439,6 @@ 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.
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
import urllib.request
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import axengine as axe
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 + ".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)
|
|
||||||
|
|
||||||
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
|
|
||||||
urllib.request.urlretrieve(
|
|
||||||
f"{HF_ENDPOINT}/AXERA-TECH/frigate-resource/resolve/axmodel/{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
web/package-lock.json
generated
72
web/package-lock.json
generated
@ -72,6 +72,8 @@
|
|||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.30.3",
|
"react-router-dom": "^6.30.3",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
|
"react-tracked": "^2.0.1",
|
||||||
|
"react-use-websocket": "^4.8.1",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
@ -4398,6 +4400,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.3.1.tgz",
|
||||||
"integrity": "sha512-LTjFz5Fk3FlbgFPJ+OJi1JdWJyiap9dSpx8W6u7JHNB7K5VbwzJe8gIU45XWLHzWFGDHKPm89VrUzjOs07TPtg==",
|
"integrity": "sha512-LTjFz5Fk3FlbgFPJ+OJi1JdWJyiap9dSpx8W6u7JHNB7K5VbwzJe8gIU45XWLHzWFGDHKPm89VrUzjOs07TPtg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
@ -4472,6 +4475,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.3.1.tgz",
|
||||||
"integrity": "sha512-ve2KHl1ITYG8QIonnuK83/T1k/5NuxP4D1egVqP9Hz2ub28kgl0rNMwmRSxXs3WIbCcMW9g3ox+daVrbSNc4Mw==",
|
"integrity": "sha512-ve2KHl1ITYG8QIonnuK83/T1k/5NuxP4D1egVqP9Hz2ub28kgl0rNMwmRSxXs3WIbCcMW9g3ox+daVrbSNc4Mw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@x0k/json-schema-merge": "^1.0.2",
|
"@x0k/json-schema-merge": "^1.0.2",
|
||||||
"fast-uri": "^3.1.0",
|
"fast-uri": "^3.1.0",
|
||||||
@ -5145,6 +5149,7 @@
|
|||||||
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@ -5154,6 +5159,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@ -5164,6 +5170,7 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@ -5293,6 +5300,7 @@
|
|||||||
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
|
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.12.0",
|
"@typescript-eslint/scope-manager": "7.12.0",
|
||||||
"@typescript-eslint/types": "7.12.0",
|
"@typescript-eslint/types": "7.12.0",
|
||||||
@ -5585,6 +5593,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -5746,6 +5755,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
|
||||||
"integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
|
"integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yr/monotone-cubic-spline": "^1.0.3",
|
"@yr/monotone-cubic-spline": "^1.0.3",
|
||||||
"svg.draggable.js": "^2.2.2",
|
"svg.draggable.js": "^2.2.2",
|
||||||
@ -5956,6 +5966,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001646",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"electron-to-chromium": "^1.5.4",
|
"electron-to-chromium": "^1.5.4",
|
||||||
@ -6456,6 +6467,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@ -6895,6 +6907,7 @@
|
|||||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@ -6950,6 +6963,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@ -7873,6 +7887,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
},
|
},
|
||||||
@ -8518,7 +8533,8 @@
|
|||||||
"url": "https://github.com/sponsors/lavrton"
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
@ -9667,7 +9683,8 @@
|
|||||||
"version": "0.52.2",
|
"version": "0.52.2",
|
||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/monaco-languageserver-types": {
|
"node_modules/monaco-languageserver-types": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@ -10375,6 +10392,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -10509,6 +10527,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@ -10657,6 +10676,11 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-compare": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w=="
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@ -10709,6 +10733,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -10773,6 +10798,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@ -10834,6 +10860,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz",
|
||||||
"integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==",
|
"integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22.0"
|
"node": ">=12.22.0"
|
||||||
},
|
},
|
||||||
@ -11088,6 +11115,29 @@
|
|||||||
"react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc"
|
"react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-tracked": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-qjbmtkO2IcW+rB2cFskRWDTjKs/w9poxvNnduacjQA04LWxOoLy9J8WfIEq1ahifQ/tVJQECrQPBm+UEzKRDtg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"proxy-compare": "^3.0.0",
|
||||||
|
"use-context-selector": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"scheduler": ">=0.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-use-websocket": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-FTXuG5O+LFozmu1BRfrzl7UIQngECvGJmL7BHsK4TYXuVt+mCizVA8lT0hGSIF0Z0TedF7bOo1nRzOUdginhDw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 18.0.0",
|
||||||
|
"react-dom": ">= 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-zoom-pan-pinch": {
|
"node_modules/react-zoom-pan-pinch": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
|
||||||
@ -11499,7 +11549,8 @@
|
|||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/scroll-into-view-if-needed": {
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@ -11998,6 +12049,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
|
||||||
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
|
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@ -12180,6 +12232,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -12358,6 +12411,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -12573,6 +12627,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-context-selector": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"scheduler": ">=0.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-long-press": {
|
"node_modules/use-long-press": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz",
|
||||||
@ -12708,6 +12771,7 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@ -12832,6 +12896,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -12845,6 +12910,7 @@
|
|||||||
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "3.0.7",
|
"@vitest/expect": "3.0.7",
|
||||||
"@vitest/mocker": "3.0.7",
|
"@vitest/mocker": "3.0.7",
|
||||||
|
|||||||
@ -78,6 +78,8 @@
|
|||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.30.3",
|
"react-router-dom": "^6.30.3",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
|
"react-tracked": "^2.0.1",
|
||||||
|
"react-use-websocket": "^4.8.1",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
|
|||||||
23
web/patches/react-use-websocket+4.8.1.patch
Normal file
23
web/patches/react-use-websocket+4.8.1.patch
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
diff --git a/node_modules/react-use-websocket/dist/lib/use-websocket.js b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||||
|
index f01db48..b30aff2 100644
|
||||||
|
--- a/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||||
|
+++ b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
||||||
|
@@ -139,15 +139,15 @@ var useWebSocket = function (url, options, connect) {
|
||||||
|
}
|
||||||
|
protectedSetLastMessage = function (message) {
|
||||||
|
if (!expectClose_1) {
|
||||||
|
- (0, react_dom_1.flushSync)(function () { return setLastMessage(message); });
|
||||||
|
+ setLastMessage(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
protectedSetReadyState = function (state) {
|
||||||
|
if (!expectClose_1) {
|
||||||
|
- (0, react_dom_1.flushSync)(function () { return setReadyState(function (prev) {
|
||||||
|
+ setReadyState(function (prev) {
|
||||||
|
var _a;
|
||||||
|
return (__assign(__assign({}, prev), (convertedUrl.current && (_a = {}, _a[convertedUrl.current] = state, _a))));
|
||||||
|
- }); });
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (createOrJoin_1) {
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import { baseUrl } from "./baseUrl";
|
|
||||||
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
|
||||||
import { WsSendContext } from "./wsContext";
|
|
||||||
import type { Update } from "./wsContext";
|
|
||||||
import { processWsMessage, resetWsStore } from "./ws";
|
|
||||||
|
|
||||||
export function WsProvider({ children }: { children: ReactNode }) {
|
|
||||||
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const reconnectAttempt = useRef(0);
|
|
||||||
const unmounted = useRef(false);
|
|
||||||
|
|
||||||
const sendJsonMessage = useCallback((msg: unknown) => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.send(JSON.stringify(msg));
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
unmounted.current = false;
|
|
||||||
|
|
||||||
function connect() {
|
|
||||||
if (unmounted.current) return;
|
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
wsRef.current = ws;
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
reconnectAttempt.current = 0;
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({ topic: "onConnect", message: "", retain: false }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event: MessageEvent) => {
|
|
||||||
processWsMessage(event.data as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
if (unmounted.current) return;
|
|
||||||
const delay = Math.min(1000 * 2 ** reconnectAttempt.current, 30000);
|
|
||||||
reconnectAttempt.current++;
|
|
||||||
reconnectTimer.current = setTimeout(connect, delay);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = () => {
|
|
||||||
ws.close();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unmounted.current = true;
|
|
||||||
if (reconnectTimer.current) {
|
|
||||||
clearTimeout(reconnectTimer.current);
|
|
||||||
}
|
|
||||||
wsRef.current?.close();
|
|
||||||
resetWsStore();
|
|
||||||
};
|
|
||||||
}, [wsUrl]);
|
|
||||||
|
|
||||||
const send = useCallback(
|
|
||||||
(message: Update) => {
|
|
||||||
sendJsonMessage({
|
|
||||||
topic: message.topic,
|
|
||||||
payload: message.payload,
|
|
||||||
retain: message.retain,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[sendJsonMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WsSendContext.Provider value={send}>{children}</WsSendContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { baseUrl } from "./baseUrl";
|
import { baseUrl } from "./baseUrl";
|
||||||
import { SWRConfig } from "swr";
|
import { SWRConfig } from "swr";
|
||||||
import { WsProvider } from "./WsProvider";
|
import { WsProvider } from "./ws";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect";
|
import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect";
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import {
|
import { baseUrl } from "./baseUrl";
|
||||||
useCallback,
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
useContext,
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useSyncExternalStore,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
EmbeddingsReindexProgressType,
|
EmbeddingsReindexProgressType,
|
||||||
FrigateCameraState,
|
FrigateCameraState,
|
||||||
@ -19,11 +14,8 @@ import {
|
|||||||
Job,
|
Job,
|
||||||
} from "@/types/ws";
|
} from "@/types/ws";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { isEqual } from "lodash";
|
import { createContainer } from "react-tracked";
|
||||||
import { WsSendContext } from "./wsContext";
|
import useDeepMemo from "@/hooks/use-deep-memo";
|
||||||
import type { Update, WsSend } from "./wsContext";
|
|
||||||
|
|
||||||
export type { Update };
|
|
||||||
|
|
||||||
export type WsFeedMessage = {
|
export type WsFeedMessage = {
|
||||||
topic: string;
|
topic: string;
|
||||||
@ -32,125 +24,55 @@ export type WsFeedMessage = {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Update = {
|
||||||
|
topic: string;
|
||||||
|
payload: unknown;
|
||||||
|
retain: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type WsState = {
|
type WsState = {
|
||||||
[topic: string]: unknown;
|
[topic: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
// External store for WebSocket state using useSyncExternalStore
|
type useValueReturn = [WsState, (update: Update) => void];
|
||||||
type Listener = () => void;
|
|
||||||
|
|
||||||
const wsState: WsState = {};
|
|
||||||
const wsTopicListeners = new Map<string, Set<Listener>>();
|
|
||||||
|
|
||||||
// Reset all module-level state. Called on WsProvider unmount to prevent
|
|
||||||
// stale data from leaking across mount/unmount cycles (e.g. HMR, logout)
|
|
||||||
export function resetWsStore() {
|
|
||||||
for (const key of Object.keys(wsState)) {
|
|
||||||
delete wsState[key];
|
|
||||||
}
|
|
||||||
wsTopicListeners.clear();
|
|
||||||
lastCameraActivityPayload = null;
|
|
||||||
wsMessageSubscribers.clear();
|
|
||||||
wsMessageIdCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and apply a raw WS message synchronously.
|
|
||||||
// Called directly from WsProvider's onmessage handler.
|
|
||||||
export function processWsMessage(raw: string) {
|
|
||||||
const data: Update = JSON.parse(raw);
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
const { topic, payload } = data;
|
|
||||||
|
|
||||||
if (topic === "camera_activity") {
|
|
||||||
applyCameraActivity(payload as string);
|
|
||||||
} else {
|
|
||||||
applyTopicUpdate(topic, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsMessageSubscribers.size > 0) {
|
|
||||||
wsMessageSubscribers.forEach((cb) =>
|
|
||||||
cb({
|
|
||||||
topic,
|
|
||||||
payload,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
id: String(wsMessageIdCounter++),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTopicUpdate(topic: string, newVal: unknown) {
|
|
||||||
const oldVal = wsState[topic];
|
|
||||||
// Fast path: === for primitives ("ON"/"OFF", numbers).
|
|
||||||
// Fall back to isEqual for objects/arrays.
|
|
||||||
const unchanged =
|
|
||||||
oldVal === newVal ||
|
|
||||||
(typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal));
|
|
||||||
if (unchanged) return;
|
|
||||||
|
|
||||||
wsState[topic] = newVal;
|
|
||||||
// Snapshot the Set — a listener may trigger unmount that modifies it.
|
|
||||||
const listeners = wsTopicListeners.get(topic);
|
|
||||||
if (listeners) {
|
|
||||||
for (const l of Array.from(listeners)) l();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscriptions
|
|
||||||
|
|
||||||
export function subscribeWsTopic(
|
|
||||||
topic: string,
|
|
||||||
listener: Listener,
|
|
||||||
): () => void {
|
|
||||||
let set = wsTopicListeners.get(topic);
|
|
||||||
if (!set) {
|
|
||||||
set = new Set();
|
|
||||||
wsTopicListeners.set(topic, set);
|
|
||||||
}
|
|
||||||
set.add(listener);
|
|
||||||
return () => {
|
|
||||||
set!.delete(listener);
|
|
||||||
if (set!.size === 0) wsTopicListeners.delete(topic);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWsTopicValue(topic: string): unknown {
|
|
||||||
return wsState[topic];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feed message subscribers
|
|
||||||
const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>();
|
const wsMessageSubscribers = new Set<(msg: WsFeedMessage) => void>();
|
||||||
let wsMessageIdCounter = 0;
|
let wsMessageIdCounter = 0;
|
||||||
|
|
||||||
// Camera activity expansion
|
function useValue(): useValueReturn {
|
||||||
//
|
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
|
||||||
// Cache the last raw camera_activity JSON string so we can skip JSON.parse
|
|
||||||
// and the entire expansion when nothing has changed. This avoids creating
|
|
||||||
// fresh objects (which defeat Object.is and force expensive isEqual deep
|
|
||||||
// traversals) on every flush — critical with many cameras.
|
|
||||||
let lastCameraActivityPayload: string | null = null;
|
|
||||||
|
|
||||||
function applyCameraActivity(payload: string) {
|
// main state
|
||||||
// Fast path: if the raw JSON string is identical, nothing changed.
|
|
||||||
if (payload === lastCameraActivityPayload) return;
|
|
||||||
lastCameraActivityPayload = payload;
|
|
||||||
|
|
||||||
let activity: { [key: string]: Partial<FrigateCameraState> };
|
const [wsState, setWsState] = useState<WsState>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activityValue: string = wsState["camera_activity"] as string;
|
||||||
|
|
||||||
|
if (!activityValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cameraActivity: { [key: string]: Partial<FrigateCameraState> };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activity = JSON.parse(payload);
|
cameraActivity = JSON.parse(activityValue);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(activity).length === 0) return;
|
if (Object.keys(cameraActivity).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [name, state] of Object.entries(activity)) {
|
const cameraStates: WsState = {};
|
||||||
applyTopicUpdate(`camera_activity/${name}`, state);
|
|
||||||
|
|
||||||
|
Object.entries(cameraActivity).forEach(([name, state]) => {
|
||||||
const cameraConfig = state?.config;
|
const cameraConfig = state?.config;
|
||||||
if (!cameraConfig) continue;
|
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
record,
|
record,
|
||||||
@ -167,69 +89,105 @@ function applyCameraActivity(payload: string) {
|
|||||||
object_descriptions,
|
object_descriptions,
|
||||||
review_descriptions,
|
review_descriptions,
|
||||||
} = cameraConfig;
|
} = cameraConfig;
|
||||||
|
cameraStates[`${name}/recordings/state`] = record ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/enabled/state`] = enabled ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/detect/state`] = detect ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/snapshots/state`] = snapshots ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/audio/state`] = audio ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/audio_transcription/state`] = audio_transcription
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/notifications/state`] = notifications
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/notifications/suspended`] =
|
||||||
|
notifications_suspended || 0;
|
||||||
|
cameraStates[`${name}/ptz_autotracker/state`] = autotracking
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/review_alerts/state`] = alerts ? "ON" : "OFF";
|
||||||
|
cameraStates[`${name}/review_detections/state`] = detections
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/object_descriptions/state`] = object_descriptions
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
cameraStates[`${name}/review_descriptions/state`] = review_descriptions
|
||||||
|
? "ON"
|
||||||
|
: "OFF";
|
||||||
|
});
|
||||||
|
|
||||||
applyTopicUpdate(`${name}/recordings/state`, record ? "ON" : "OFF");
|
setWsState((prevState) => ({
|
||||||
applyTopicUpdate(`${name}/enabled/state`, enabled ? "ON" : "OFF");
|
...prevState,
|
||||||
applyTopicUpdate(`${name}/detect/state`, detect ? "ON" : "OFF");
|
...cameraStates,
|
||||||
applyTopicUpdate(`${name}/snapshots/state`, snapshots ? "ON" : "OFF");
|
}));
|
||||||
applyTopicUpdate(`${name}/audio/state`, audio ? "ON" : "OFF");
|
|
||||||
applyTopicUpdate(
|
// we only want this to run initially when the config is loaded
|
||||||
`${name}/audio_transcription/state`,
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
audio_transcription ? "ON" : "OFF",
|
}, [wsState["camera_activity"]]);
|
||||||
);
|
|
||||||
applyTopicUpdate(
|
// ws handler
|
||||||
`${name}/notifications/state`,
|
const { sendJsonMessage, readyState } = useWebSocket(wsUrl, {
|
||||||
notifications ? "ON" : "OFF",
|
onMessage: (event) => {
|
||||||
);
|
const data: Update = JSON.parse(event.data);
|
||||||
applyTopicUpdate(
|
|
||||||
`${name}/notifications/suspended`,
|
if (data) {
|
||||||
notifications_suspended || 0,
|
setWsState((prevState) => ({
|
||||||
);
|
...prevState,
|
||||||
applyTopicUpdate(
|
[data.topic]: data.payload,
|
||||||
`${name}/ptz_autotracker/state`,
|
}));
|
||||||
autotracking ? "ON" : "OFF",
|
|
||||||
);
|
// Notify feed subscribers
|
||||||
applyTopicUpdate(`${name}/review_alerts/state`, alerts ? "ON" : "OFF");
|
if (wsMessageSubscribers.size > 0) {
|
||||||
applyTopicUpdate(
|
const feedMsg: WsFeedMessage = {
|
||||||
`${name}/review_detections/state`,
|
topic: data.topic,
|
||||||
detections ? "ON" : "OFF",
|
payload: data.payload,
|
||||||
);
|
timestamp: Date.now(),
|
||||||
applyTopicUpdate(
|
id: String(wsMessageIdCounter++),
|
||||||
`${name}/object_descriptions/state`,
|
};
|
||||||
object_descriptions ? "ON" : "OFF",
|
wsMessageSubscribers.forEach((cb) => cb(feedMsg));
|
||||||
);
|
|
||||||
applyTopicUpdate(
|
|
||||||
`${name}/review_descriptions/state`,
|
|
||||||
review_descriptions ? "ON" : "OFF",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpen: () => {
|
||||||
|
sendJsonMessage({
|
||||||
|
topic: "onConnect",
|
||||||
|
message: "",
|
||||||
|
retain: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onClose: () => {},
|
||||||
|
shouldReconnect: () => true,
|
||||||
|
retryOnError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setState = useCallback(
|
||||||
|
(message: Update) => {
|
||||||
|
if (readyState === ReadyState.OPEN) {
|
||||||
|
sendJsonMessage({
|
||||||
|
topic: message.topic,
|
||||||
|
payload: message.payload,
|
||||||
|
retain: message.retain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[readyState, sendJsonMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [wsState, setState];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks
|
export const {
|
||||||
export function useWsUpdate(): WsSend {
|
Provider: WsProvider,
|
||||||
const send = useContext(WsSendContext);
|
useTrackedState: useWsState,
|
||||||
if (!send) {
|
useUpdate: useWsUpdate,
|
||||||
throw new Error("useWsUpdate must be used within WsProvider");
|
} = createContainer(useValue, { defaultState: {}, concurrentMode: true });
|
||||||
}
|
|
||||||
return send;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to a single WS topic with proper bail-out.
|
|
||||||
// Only re-renders when the topic's value changes (Object.is comparison).
|
|
||||||
// Uses useSyncExternalStore — zero useEffect, so no PassiveMask flags
|
|
||||||
// propagate through the fiber tree.
|
|
||||||
export function useWs(watchTopic: string, publishTopic: string) {
|
export function useWs(watchTopic: string, publishTopic: string) {
|
||||||
const payload = useSyncExternalStore(
|
const state = useWsState();
|
||||||
useCallback(
|
|
||||||
(listener: Listener) => subscribeWsTopic(watchTopic, listener),
|
|
||||||
[watchTopic],
|
|
||||||
),
|
|
||||||
useCallback(() => wsState[watchTopic], [watchTopic]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sendJsonMessage = useWsUpdate();
|
const sendJsonMessage = useWsUpdate();
|
||||||
|
|
||||||
const value = { payload: payload ?? null };
|
const value = { payload: state[watchTopic] || null };
|
||||||
|
|
||||||
const send = useCallback(
|
const send = useCallback(
|
||||||
(payload: unknown, retain = false) => {
|
(payload: unknown, retain = false) => {
|
||||||
@ -245,8 +203,6 @@ export function useWs(watchTopic: string, publishTopic: string) {
|
|||||||
return { value, send };
|
return { value, send };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience hooks
|
|
||||||
|
|
||||||
export function useEnabledState(camera: string): {
|
export function useEnabledState(camera: string): {
|
||||||
payload: ToggleableSetting;
|
payload: ToggleableSetting;
|
||||||
send: (payload: ToggleableSetting, retain?: boolean) => void;
|
send: (payload: ToggleableSetting, retain?: boolean) => void;
|
||||||
@ -457,42 +413,28 @@ export function useFrigateEvents(): { payload: FrigateEvent } {
|
|||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("events", "");
|
} = useWs("events", "");
|
||||||
const parsed = useMemo(
|
return { payload: JSON.parse(payload as string) };
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
return { payload: parsed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAudioDetections(): { payload: FrigateAudioDetections } {
|
export function useAudioDetections(): { payload: FrigateAudioDetections } {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("audio_detections", "");
|
} = useWs("audio_detections", "");
|
||||||
const parsed = useMemo(
|
return { payload: JSON.parse(payload as string) };
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
return { payload: parsed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFrigateReviews(): FrigateReview {
|
export function useFrigateReviews(): FrigateReview {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("reviews", "");
|
} = useWs("reviews", "");
|
||||||
return useMemo(
|
return useDeepMemo(JSON.parse(payload as string));
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFrigateStats(): FrigateStats {
|
export function useFrigateStats(): FrigateStats {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("stats", "");
|
} = useWs("stats", "");
|
||||||
return useMemo(
|
return useDeepMemo(JSON.parse(payload as string));
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialCameraState(
|
export function useInitialCameraState(
|
||||||
@ -504,31 +446,32 @@ export function useInitialCameraState(
|
|||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs(`camera_activity/${camera}`, "onConnect");
|
} = useWs("camera_activity", "onConnect");
|
||||||
|
|
||||||
// camera_activity sub-topic payload is already parsed by expandCameraActivity
|
const data = useDeepMemo(JSON.parse(payload as string));
|
||||||
const data = payload as FrigateCameraState | undefined;
|
|
||||||
|
|
||||||
// onConnect is sent once in WsProvider.onopen — no need to re-request on
|
|
||||||
// every component mount. Components read cached wsState immediately via
|
|
||||||
// useSyncExternalStore. Only re-request when the user tabs back in.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!revalidateOnFocus) return;
|
let listener = undefined;
|
||||||
|
if (revalidateOnFocus) {
|
||||||
const listener = () => {
|
sendCommand("onConnect");
|
||||||
if (document.visibilityState === "visible") {
|
listener = () => {
|
||||||
|
if (document.visibilityState == "visible") {
|
||||||
sendCommand("onConnect");
|
sendCommand("onConnect");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
addEventListener("visibilitychange", listener);
|
addEventListener("visibilitychange", listener);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (listener) {
|
||||||
removeEventListener("visibilitychange", listener);
|
removeEventListener("visibilitychange", listener);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
// only refresh when onRefresh value changes
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [revalidateOnFocus]);
|
}, [revalidateOnFocus]);
|
||||||
|
|
||||||
return { payload: data as FrigateCameraState };
|
return { payload: data ? data[camera] : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModelState(
|
export function useModelState(
|
||||||
@ -540,10 +483,7 @@ export function useModelState(
|
|||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs("model_state", "modelState");
|
} = useWs("model_state", "modelState");
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useDeepMemo(JSON.parse(payload as string));
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let listener = undefined;
|
let listener = undefined;
|
||||||
@ -579,10 +519,7 @@ export function useEmbeddingsReindexProgress(
|
|||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs("embeddings_reindex_progress", "embeddingsReindexProgress");
|
} = useWs("embeddings_reindex_progress", "embeddingsReindexProgress");
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useDeepMemo(JSON.parse(payload as string));
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let listener = undefined;
|
let listener = undefined;
|
||||||
@ -616,9 +553,8 @@ export function useAudioTranscriptionProcessState(
|
|||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs("audio_transcription_state", "audioTranscriptionState");
|
} = useWs("audio_transcription_state", "audioTranscriptionState");
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useDeepMemo(
|
||||||
() => (payload ? (JSON.parse(payload as string) as string) : "idle"),
|
payload ? (JSON.parse(payload as string) as string) : "idle",
|
||||||
[payload],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -651,10 +587,7 @@ export function useBirdseyeLayout(revalidateOnFocus: boolean = true): {
|
|||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs("birdseye_layout", "birdseyeLayout");
|
} = useWs("birdseye_layout", "birdseyeLayout");
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useDeepMemo(JSON.parse(payload as string));
|
||||||
() => (payload ? JSON.parse(payload as string) : undefined),
|
|
||||||
[payload],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let listener = undefined;
|
let listener = undefined;
|
||||||
@ -751,14 +684,10 @@ export function useTrackedObjectUpdate(): {
|
|||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("tracked_object_update", "");
|
} = useWs("tracked_object_update", "");
|
||||||
const parsed = useMemo(
|
const parsed = payload
|
||||||
() =>
|
|
||||||
payload
|
|
||||||
? JSON.parse(payload as string)
|
? JSON.parse(payload as string)
|
||||||
: { type: "", id: "", camera: "" },
|
: { type: "", id: "", camera: "" };
|
||||||
[payload],
|
return { payload: useDeepMemo(parsed) };
|
||||||
);
|
|
||||||
return { payload: parsed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNotifications(camera: string): {
|
export function useNotifications(camera: string): {
|
||||||
@ -801,14 +730,10 @@ export function useTriggers(): { payload: TriggerStatus } {
|
|||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("triggers", "");
|
} = useWs("triggers", "");
|
||||||
const parsed = useMemo(
|
const parsed = payload
|
||||||
() =>
|
|
||||||
payload
|
|
||||||
? JSON.parse(payload as string)
|
? JSON.parse(payload as string)
|
||||||
: { name: "", camera: "", event_id: "", type: "", score: 0 },
|
: { name: "", camera: "", event_id: "", type: "", score: 0 };
|
||||||
[payload],
|
return { payload: useDeepMemo(parsed) };
|
||||||
);
|
|
||||||
return { payload: parsed };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useJobStatus(
|
export function useJobStatus(
|
||||||
@ -820,9 +745,8 @@ export function useJobStatus(
|
|||||||
send: sendCommand,
|
send: sendCommand,
|
||||||
} = useWs("job_state", "jobState");
|
} = useWs("job_state", "jobState");
|
||||||
|
|
||||||
const jobData = useMemo(
|
const jobData = useDeepMemo(
|
||||||
() => (payload && typeof payload === "string" ? JSON.parse(payload) : {}),
|
payload && typeof payload === "string" ? JSON.parse(payload) : {},
|
||||||
[payload],
|
|
||||||
);
|
);
|
||||||
const currentJob = jobData[jobType] || null;
|
const currentJob = jobData[jobType] || null;
|
||||||
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
export type Update = {
|
|
||||||
topic: string;
|
|
||||||
payload: unknown;
|
|
||||||
retain: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WsSend = (update: Update) => void;
|
|
||||||
|
|
||||||
export const WsSendContext = createContext<WsSend | null>(null);
|
|
||||||
@ -98,10 +98,10 @@ const TimeAgo: FunctionComponent<IProp> = ({
|
|||||||
return manualRefreshInterval;
|
return manualRefreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsedMs = currentTime.getTime() - time;
|
const currentTs = currentTime.getTime() / 1000;
|
||||||
if (elapsedMs < 60000) {
|
if (currentTs - time < 60) {
|
||||||
return 1000; // refresh every second
|
return 1000; // refresh every second
|
||||||
} else if (elapsedMs < 3600000) {
|
} else if (currentTs - time < 3600) {
|
||||||
return 60000; // refresh every minute
|
return 60000; // refresh every minute
|
||||||
} else {
|
} else {
|
||||||
return 3600000; // refresh every hour
|
return 3600000; // refresh every hour
|
||||||
|
|||||||
@ -1,70 +1,18 @@
|
|||||||
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
import { useMemo } from "react";
|
||||||
import { Polygon } from "@/types/canvas";
|
import { Polygon } from "@/types/canvas";
|
||||||
import { subscribeWsTopic, getWsTopicValue } from "@/api/ws";
|
import { useWsState } from "@/api/ws";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to get enabled state for a polygon from websocket state.
|
* Hook to get enabled state for a polygon from websocket state.
|
||||||
* Subscribes to all relevant per-polygon topics so it only re-renders
|
* Memoizes the lookup function to avoid unnecessary re-renders.
|
||||||
* when one of those specific topics changes — not on every WS update.
|
|
||||||
*/
|
*/
|
||||||
export function usePolygonStates(polygons: Polygon[]) {
|
export function usePolygonStates(polygons: Polygon[]) {
|
||||||
// Build a stable sorted list of topics we need to watch
|
const wsState = useWsState();
|
||||||
const topics = useMemo(() => {
|
|
||||||
const set = new Set<string>();
|
|
||||||
polygons.forEach((polygon) => {
|
|
||||||
const topic =
|
|
||||||
polygon.type === "zone"
|
|
||||||
? `${polygon.camera}/zone/${polygon.name}/state`
|
|
||||||
: polygon.type === "motion_mask"
|
|
||||||
? `${polygon.camera}/motion_mask/${polygon.name}/state`
|
|
||||||
: `${polygon.camera}/object_mask/${polygon.name}/state`;
|
|
||||||
set.add(topic);
|
|
||||||
});
|
|
||||||
return Array.from(set).sort();
|
|
||||||
}, [polygons]);
|
|
||||||
|
|
||||||
// Stable key for the topic list so subscribe/getSnapshot stay in sync
|
// Create a memoized lookup map that only updates when relevant ws values change
|
||||||
const topicsKey = topics.join("\0");
|
|
||||||
|
|
||||||
// Subscribe to all topics at once — re-subscribe only when the set changes
|
|
||||||
const subscribe = useCallback(
|
|
||||||
(listener: () => void) => {
|
|
||||||
const unsubscribes = topicsKey
|
|
||||||
.split("\0")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((topic) => subscribeWsTopic(topic, listener));
|
|
||||||
return () => unsubscribes.forEach((unsub) => unsub());
|
|
||||||
},
|
|
||||||
[topicsKey],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build a snapshot string from the current values of all topics.
|
|
||||||
// useSyncExternalStore uses Object.is, so we return a primitive that
|
|
||||||
// changes only when an observed topic's value changes.
|
|
||||||
const getSnapshot = useCallback(() => {
|
|
||||||
return topicsKey
|
|
||||||
.split("\0")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((topic) => `${topic}=${getWsTopicValue(topic) ?? ""}`)
|
|
||||||
.join("\0");
|
|
||||||
}, [topicsKey]);
|
|
||||||
|
|
||||||
const snapshot = useSyncExternalStore(subscribe, getSnapshot);
|
|
||||||
|
|
||||||
// Parse the snapshot into a lookup map
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Build value map from snapshot
|
|
||||||
const valueMap = new Map<string, unknown>();
|
|
||||||
snapshot.split("\0").forEach((entry) => {
|
|
||||||
const eqIdx = entry.indexOf("=");
|
|
||||||
if (eqIdx > 0) {
|
|
||||||
const topic = entry.slice(0, eqIdx);
|
|
||||||
const val = entry.slice(eqIdx + 1) || undefined;
|
|
||||||
valueMap.set(topic, val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const stateMap = new Map<string, boolean>();
|
const stateMap = new Map<string, boolean>();
|
||||||
|
|
||||||
polygons.forEach((polygon) => {
|
polygons.forEach((polygon) => {
|
||||||
const topic =
|
const topic =
|
||||||
polygon.type === "zone"
|
polygon.type === "zone"
|
||||||
@ -73,7 +21,7 @@ export function usePolygonStates(polygons: Polygon[]) {
|
|||||||
? `${polygon.camera}/motion_mask/${polygon.name}/state`
|
? `${polygon.camera}/motion_mask/${polygon.name}/state`
|
||||||
: `${polygon.camera}/object_mask/${polygon.name}/state`;
|
: `${polygon.camera}/object_mask/${polygon.name}/state`;
|
||||||
|
|
||||||
const wsValue = valueMap.get(topic);
|
const wsValue = wsState[topic];
|
||||||
const enabled =
|
const enabled =
|
||||||
wsValue === "ON"
|
wsValue === "ON"
|
||||||
? true
|
? true
|
||||||
@ -92,5 +40,5 @@ export function usePolygonStates(polygons: Polygon[]) {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [polygons, snapshot]);
|
}, [polygons, wsState]);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user