mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-09-05 17:51:36 +02:00
feat: reload go2rtc config
This commit is contained in:
parent
78e431d5e3
commit
85741c02e4
@ -32,6 +32,7 @@ from frigate.config import FrigateConfig
|
||||
from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateEnum,
|
||||
CameraConfigUpdateTopic,
|
||||
reload_go2rtc,
|
||||
)
|
||||
from frigate.models import Event, Timeline
|
||||
from frigate.stats.prometheus import get_metrics, update_metrics
|
||||
@ -424,15 +425,38 @@ def config_set(request: Request, body: AppConfigSetBody):
|
||||
|
||||
if field == "add":
|
||||
settings = config.cameras[camera]
|
||||
if camera in old_config.cameras:
|
||||
request.app.config_publisher.publish_update(
|
||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum.edit, camera),
|
||||
settings,
|
||||
)
|
||||
continue_publish = False
|
||||
else:
|
||||
continue_publish = True
|
||||
elif field == "edit":
|
||||
settings = config.cameras[camera]
|
||||
if camera in old_config.cameras:
|
||||
logger.info(f"Publishing edit event for camera {camera}")
|
||||
continue_publish = True
|
||||
else:
|
||||
request.app.config_publisher.publish_update(
|
||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum.add, camera),
|
||||
settings,
|
||||
)
|
||||
continue_publish = False
|
||||
elif field == "remove":
|
||||
settings = old_config.cameras[camera]
|
||||
continue_publish = True
|
||||
else:
|
||||
settings = config.get_nested_object(body.update_topic)
|
||||
continue_publish = True
|
||||
|
||||
request.app.config_publisher.publish_update(
|
||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera),
|
||||
settings,
|
||||
)
|
||||
if continue_publish:
|
||||
logger.info(f"Publishing {field} event for camera {camera}")
|
||||
request.app.config_publisher.publish_update(
|
||||
CameraConfigUpdateTopic(CameraConfigUpdateEnum[field], camera),
|
||||
settings,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content=(
|
||||
@ -530,6 +554,23 @@ def nvinfo():
|
||||
return JSONResponse(content=get_nvidia_driver_info())
|
||||
|
||||
|
||||
@router.post("/go2rtc/reload", dependencies=[Depends(require_role(["admin"]))])
|
||||
def reload_go2rtc_endpoint():
|
||||
"""reload go2rtc service"""
|
||||
try:
|
||||
reload_go2rtc()
|
||||
return JSONResponse(
|
||||
content={"success": True, "message": "go2rtc reloaded"},
|
||||
status_code=200,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"reload go2rtc failed: {e}")
|
||||
return JSONResponse(
|
||||
content={"success": False, "message": f"{str(e)}"},
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/logs/{service}", tags=[Tags.logs])
|
||||
async def logs(
|
||||
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
|
||||
|
@ -50,6 +50,7 @@ class CameraMaintainer(threading.Thread):
|
||||
[
|
||||
CameraConfigUpdateEnum.add,
|
||||
CameraConfigUpdateEnum.remove,
|
||||
CameraConfigUpdateEnum.edit,
|
||||
],
|
||||
)
|
||||
self.shm_count = self.__calculate_shm_frame_count()
|
||||
@ -194,6 +195,21 @@ class CameraMaintainer(threading.Thread):
|
||||
for update_type, updated_cameras in updates.items():
|
||||
if update_type == CameraConfigUpdateEnum.add.name:
|
||||
for camera in updated_cameras:
|
||||
self.__start_camera_processor(
|
||||
camera,
|
||||
self.update_subscriber.camera_configs[camera],
|
||||
runtime=True,
|
||||
)
|
||||
self.__start_camera_capture(
|
||||
camera,
|
||||
self.update_subscriber.camera_configs[camera],
|
||||
runtime=True,
|
||||
)
|
||||
elif update_type == CameraConfigUpdateEnum.edit.name:
|
||||
for camera in updated_cameras:
|
||||
self.__stop_camera_capture_process(camera)
|
||||
self.__stop_camera_process(camera)
|
||||
|
||||
self.__start_camera_processor(
|
||||
camera,
|
||||
self.update_subscriber.camera_configs[camera],
|
||||
@ -205,8 +221,9 @@ class CameraMaintainer(threading.Thread):
|
||||
runtime=True,
|
||||
)
|
||||
elif update_type == CameraConfigUpdateEnum.remove.name:
|
||||
self.__stop_camera_capture_process(camera)
|
||||
self.__stop_camera_process(camera)
|
||||
for camera in updated_cameras:
|
||||
self.__stop_camera_capture_process(camera)
|
||||
self.__stop_camera_process(camera)
|
||||
|
||||
# ensure the capture processes are done
|
||||
for camera in self.camera_processes.keys():
|
||||
|
@ -1,11 +1,20 @@
|
||||
"""Convenience classes for updating configurations dynamically."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from frigate.comms.config_updater import ConfigPublisher, ConfigSubscriber
|
||||
from frigate.config import CameraConfig, FrigateConfig
|
||||
from frigate.util.builtin import update_yaml_file_bulk
|
||||
from frigate.util.config import find_config_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CameraConfigUpdateEnum(str, Enum):
|
||||
@ -16,6 +25,7 @@ class CameraConfigUpdateEnum(str, Enum):
|
||||
audio_transcription = "audio_transcription"
|
||||
birdseye = "birdseye"
|
||||
detect = "detect"
|
||||
edit = "edit" # for editing an existing camera
|
||||
enabled = "enabled"
|
||||
motion = "motion" # includes motion and motion masks
|
||||
notifications = "notifications"
|
||||
@ -76,37 +86,61 @@ class CameraConfigUpdateSubscriber:
|
||||
self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any
|
||||
) -> None:
|
||||
if update_type == CameraConfigUpdateEnum.add:
|
||||
"""Add a new camera and handle go2rtc streams if needed"""
|
||||
self.config.cameras[camera] = updated_config
|
||||
self.camera_configs[camera] = updated_config
|
||||
|
||||
# Handle potential go2rtc streams for new camera
|
||||
if hasattr(updated_config, "ffmpeg") and hasattr(
|
||||
updated_config.ffmpeg, "inputs"
|
||||
):
|
||||
streams = self._extract_stream_names(updated_config)
|
||||
if (
|
||||
streams
|
||||
and hasattr(self.config, "go2rtc")
|
||||
and hasattr(self.config.go2rtc, "streams")
|
||||
):
|
||||
go2rtc_updated = False
|
||||
for stream_name in streams:
|
||||
logger.info(
|
||||
f"New go2rtc stream detected for camera {camera}: {stream_name}"
|
||||
)
|
||||
go2rtc_updated = True
|
||||
|
||||
if go2rtc_updated:
|
||||
self.reload_go2rtc()
|
||||
|
||||
return
|
||||
elif update_type == CameraConfigUpdateEnum.edit:
|
||||
"""Update camera configuration without stopping processes"""
|
||||
old_config = self.camera_configs.get(camera)
|
||||
|
||||
self._handle_go2rtc_stream_updates(camera, old_config, updated_config)
|
||||
|
||||
self.config.cameras[camera] = updated_config
|
||||
self.camera_configs[camera] = updated_config
|
||||
|
||||
return
|
||||
elif update_type == CameraConfigUpdateEnum.remove:
|
||||
"""Remove go2rtc streams with camera"""
|
||||
camera_config = self.camera_configs.get(camera)
|
||||
if (
|
||||
camera_config
|
||||
and hasattr(self.config, "go2rtc")
|
||||
and hasattr(camera_config, "ffmpeg")
|
||||
and hasattr(camera_config.ffmpeg, "inputs")
|
||||
):
|
||||
for input_item in camera_config.ffmpeg.inputs:
|
||||
if hasattr(input_item, "path") and isinstance(input_item.path, str):
|
||||
if (
|
||||
"rtsp://" in input_item.path
|
||||
and "127.0.0.1:8554/" in input_item.path
|
||||
):
|
||||
stream_name = (
|
||||
input_item.path.split("127.0.0.1:8554/")[-1]
|
||||
.split("&")[0]
|
||||
.split("?")[0]
|
||||
)
|
||||
if (
|
||||
hasattr(self.config.go2rtc, "streams")
|
||||
and stream_name in self.config.go2rtc.streams
|
||||
):
|
||||
self.config.go2rtc.streams.pop(stream_name)
|
||||
go2rtc_updated = False
|
||||
|
||||
# Use helper methods to handle go2rtc streams
|
||||
if camera_config:
|
||||
streams = self._extract_stream_names(camera_config)
|
||||
for stream_name in streams:
|
||||
if not self._is_stream_in_use(stream_name, camera):
|
||||
if self._remove_unused_stream(stream_name):
|
||||
go2rtc_updated = True
|
||||
|
||||
if go2rtc_updated:
|
||||
self.reload_go2rtc()
|
||||
|
||||
self.camera_configs.pop(camera, None)
|
||||
if camera in self.config.cameras:
|
||||
self.config.cameras.pop(camera)
|
||||
|
||||
self.config.cameras.pop(camera)
|
||||
self.camera_configs.pop(camera)
|
||||
return
|
||||
|
||||
config = self.camera_configs.get(camera)
|
||||
@ -170,3 +204,138 @@ class CameraConfigUpdateSubscriber:
|
||||
|
||||
def stop(self) -> None:
|
||||
self.subscriber.stop()
|
||||
|
||||
def _extract_stream_names(self, config):
|
||||
"""Extract go2rtc stream names from configuration"""
|
||||
streams = []
|
||||
|
||||
if not (hasattr(config, "ffmpeg") and hasattr(config.ffmpeg, "inputs")):
|
||||
return streams
|
||||
|
||||
for input_item in config.ffmpeg.inputs:
|
||||
if not (hasattr(input_item, "path") and isinstance(input_item.path, str)):
|
||||
continue
|
||||
|
||||
path = input_item.path
|
||||
if "rtsp://" in path and "127.0.0.1:8554/" in path:
|
||||
stream_name = (
|
||||
path.split("127.0.0.1:8554/")[-1].split("&")[0].split("?")[0]
|
||||
)
|
||||
streams.append(stream_name)
|
||||
|
||||
return streams
|
||||
|
||||
def _is_stream_in_use(self, stream_name, current_camera):
|
||||
"""Check if stream is used by other cameras"""
|
||||
for cam_name, cam_config in self.camera_configs.items():
|
||||
if cam_name == current_camera:
|
||||
continue
|
||||
|
||||
if not (
|
||||
hasattr(cam_config, "ffmpeg") and hasattr(cam_config.ffmpeg, "inputs")
|
||||
):
|
||||
continue
|
||||
|
||||
for input_item in cam_config.ffmpeg.inputs:
|
||||
if (
|
||||
hasattr(input_item, "path")
|
||||
and isinstance(input_item.path, str)
|
||||
and "127.0.0.1:8554/" + stream_name in input_item.path
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _remove_unused_stream(self, stream_name):
|
||||
"""Remove unused go2rtc stream"""
|
||||
if not (
|
||||
hasattr(self.config, "go2rtc")
|
||||
and hasattr(self.config.go2rtc, "streams")
|
||||
and stream_name in self.config.go2rtc.streams
|
||||
):
|
||||
return False
|
||||
|
||||
logger.info(f"Removing unused go2rtc stream: {stream_name}")
|
||||
self.config.go2rtc.streams.pop(stream_name)
|
||||
config_file = find_config_file()
|
||||
updates: Dict[str, Any] = {f"go2rtc.streams.{stream_name}": ""}
|
||||
update_yaml_file_bulk(config_file, updates)
|
||||
return True
|
||||
|
||||
def _handle_go2rtc_stream_updates(self, camera, old_config, updated_config):
|
||||
"""Handle go2rtc stream configuration updates"""
|
||||
if not (
|
||||
old_config
|
||||
and hasattr(self.config, "go2rtc")
|
||||
and hasattr(self.config.go2rtc, "streams")
|
||||
):
|
||||
return
|
||||
|
||||
old_streams = self._extract_stream_names(old_config)
|
||||
new_streams = self._extract_stream_names(updated_config)
|
||||
|
||||
if not (old_streams or new_streams):
|
||||
return
|
||||
|
||||
removed_streams = [s for s in old_streams if s not in new_streams]
|
||||
added_streams = [s for s in new_streams if s not in old_streams]
|
||||
common_streams = [s for s in old_streams if s in new_streams]
|
||||
|
||||
go2rtc_updated = False
|
||||
|
||||
for stream_name in added_streams:
|
||||
logger.info(f"New go2rtc stream detected: {stream_name}")
|
||||
go2rtc_updated = True
|
||||
|
||||
for stream_name in removed_streams:
|
||||
if not self._is_stream_in_use(stream_name, camera):
|
||||
if self._remove_unused_stream(stream_name):
|
||||
go2rtc_updated = True
|
||||
|
||||
for stream_name in common_streams:
|
||||
if stream_name in self.config.go2rtc.streams:
|
||||
for input_item in (
|
||||
updated_config.ffmpeg.inputs
|
||||
if (
|
||||
hasattr(updated_config, "ffmpeg")
|
||||
and hasattr(updated_config.ffmpeg, "inputs")
|
||||
)
|
||||
else []
|
||||
):
|
||||
if (
|
||||
hasattr(input_item, "path")
|
||||
and isinstance(input_item.path, str)
|
||||
and "127.0.0.1:8554/" + stream_name in input_item.path
|
||||
):
|
||||
logger.info(
|
||||
f"Checking for changes in go2rtc stream: {stream_name}"
|
||||
)
|
||||
go2rtc_updated = True
|
||||
break
|
||||
|
||||
if go2rtc_updated:
|
||||
self.reload_go2rtc()
|
||||
|
||||
def reload_go2rtc(self):
|
||||
"""Regenerate go2rtc config and restart the service."""
|
||||
try:
|
||||
create_config_path = "/usr/local/go2rtc/create_config.py"
|
||||
if os.path.exists(create_config_path):
|
||||
subprocess.run(["python3", create_config_path], check=True)
|
||||
logger.info("Successfully regenerated go2rtc config")
|
||||
else:
|
||||
logger.warning(f"Could not find {create_config_path}")
|
||||
|
||||
# Restart go2rtc service
|
||||
try:
|
||||
response = requests.post("http://127.0.0.1:1984/api/restart", timeout=5)
|
||||
if response.status_code == 200:
|
||||
logger.info("Successfully restarted go2rtc service")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Failed to restart go2rtc service: {response.status_code}"
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error restarting go2rtc service: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error reloading go2rtc: {e}")
|
||||
|
@ -102,6 +102,7 @@ class EmbeddingMaintainer(threading.Thread):
|
||||
[
|
||||
CameraConfigUpdateEnum.add,
|
||||
CameraConfigUpdateEnum.remove,
|
||||
CameraConfigUpdateEnum.edit,
|
||||
CameraConfigUpdateEnum.object_genai,
|
||||
CameraConfigUpdateEnum.review_genai,
|
||||
CameraConfigUpdateEnum.semantic_search,
|
||||
|
216
scripts/create_go2rtc_config.py
Normal file
216
scripts/create_go2rtc_config.py
Normal file
@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Creates a go2rtc config file with proper permissions."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from ruamel.yaml import YAML
|
||||
except ImportError:
|
||||
print("Installing required packages...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "ruamel.yaml"])
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
# Add frigate to path
|
||||
sys.path.insert(0, "/opt/frigate")
|
||||
try:
|
||||
from frigate.const import (
|
||||
BIRDSEYE_PIPE,
|
||||
DEFAULT_FFMPEG_VERSION,
|
||||
INCLUDED_FFMPEG_VERSIONS,
|
||||
LIBAVFORMAT_VERSION_MAJOR,
|
||||
)
|
||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||
from frigate.util.config import find_config_file
|
||||
except ImportError:
|
||||
# Fallback if imports fail
|
||||
print("Warning: Could not import from frigate module. Using fallback values.")
|
||||
BIRDSEYE_PIPE = "/tmp/birdseye"
|
||||
DEFAULT_FFMPEG_VERSION = "v5.1.2"
|
||||
INCLUDED_FFMPEG_VERSIONS = ["v5.1.2", "v4.4"]
|
||||
LIBAVFORMAT_VERSION_MAJOR = 59
|
||||
|
||||
def parse_preset_hardware_acceleration_encode(ffmpeg_path, hwaccel_args, input_args, output_args):
|
||||
return f"{ffmpeg_path} {hwaccel_args} {input_args} {output_args}"
|
||||
|
||||
def find_config_file():
|
||||
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
|
||||
if os.path.exists(config_file):
|
||||
return config_file
|
||||
|
||||
# Try common locations
|
||||
for ext in [".yml", ".yaml", ".json"]:
|
||||
for path in ["/config/config", "/etc/frigate/config"]:
|
||||
if os.path.exists(f"{path}{ext}"):
|
||||
return f"{path}{ext}"
|
||||
return "/config/config.yml"
|
||||
|
||||
# Remove frigate from path
|
||||
if "/opt/frigate" in sys.path:
|
||||
sys.path.remove("/opt/frigate")
|
||||
|
||||
yaml = YAML()
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets"):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = (
|
||||
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
|
||||
)
|
||||
|
||||
config_file = find_config_file()
|
||||
print(f"Using config file: {config_file}")
|
||||
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
raw_config = f.read()
|
||||
|
||||
if config_file.endswith((".yaml", ".yml")):
|
||||
config: dict[str, Any] = yaml.load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config: dict[str, Any] = json.loads(raw_config)
|
||||
except FileNotFoundError:
|
||||
print(f"Config file not found: {config_file}")
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
go2rtc_config: dict[str, Any] = config.get("go2rtc", {})
|
||||
|
||||
# Need to enable CORS for go2rtc so the frigate integration / card work automatically
|
||||
if go2rtc_config.get("api") is None:
|
||||
go2rtc_config["api"] = {"origin": "*"}
|
||||
elif go2rtc_config["api"].get("origin") is None:
|
||||
go2rtc_config["api"]["origin"] = "*"
|
||||
|
||||
# Need to set default location for HA config
|
||||
if go2rtc_config.get("hass") is None:
|
||||
go2rtc_config["hass"] = {"config": "/homeassistant"}
|
||||
|
||||
# we want to ensure that logs are easy to read
|
||||
if go2rtc_config.get("log") is None:
|
||||
go2rtc_config["log"] = {"format": "text"}
|
||||
elif go2rtc_config["log"].get("format") is None:
|
||||
go2rtc_config["log"]["format"] = "text"
|
||||
|
||||
# ensure there is a default webrtc config
|
||||
if go2rtc_config.get("webrtc") is None:
|
||||
go2rtc_config["webrtc"] = {}
|
||||
|
||||
if go2rtc_config["webrtc"].get("candidates") is None:
|
||||
default_candidates = []
|
||||
# use internal candidate if it was discovered when running through the add-on
|
||||
internal_candidate = os.environ.get("FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL")
|
||||
if internal_candidate is not None:
|
||||
default_candidates.append(internal_candidate)
|
||||
# should set default stun server so webrtc can work
|
||||
default_candidates.append("stun:8555")
|
||||
|
||||
go2rtc_config["webrtc"]["candidates"] = default_candidates
|
||||
|
||||
if go2rtc_config.get("rtsp", {}).get("username") is not None:
|
||||
go2rtc_config["rtsp"]["username"] = go2rtc_config["rtsp"]["username"].format(
|
||||
**FRIGATE_ENV_VARS
|
||||
)
|
||||
|
||||
if go2rtc_config.get("rtsp", {}).get("password") is not None:
|
||||
go2rtc_config["rtsp"]["password"] = go2rtc_config["rtsp"]["password"].format(
|
||||
**FRIGATE_ENV_VARS
|
||||
)
|
||||
|
||||
# ensure ffmpeg path is set correctly
|
||||
path = config.get("ffmpeg", {}).get("path", "default")
|
||||
if path == "default":
|
||||
ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
|
||||
elif path in INCLUDED_FFMPEG_VERSIONS:
|
||||
ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg"
|
||||
else:
|
||||
ffmpeg_path = f"{path}/bin/ffmpeg"
|
||||
|
||||
if go2rtc_config.get("ffmpeg") is None:
|
||||
go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path}
|
||||
elif go2rtc_config["ffmpeg"].get("bin") is None:
|
||||
go2rtc_config["ffmpeg"]["bin"] = ffmpeg_path
|
||||
|
||||
# need to replace ffmpeg command when using ffmpeg4
|
||||
if LIBAVFORMAT_VERSION_MAJOR < 59:
|
||||
rtsp_args = "-fflags nobuffer -flags low_delay -stimeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}"
|
||||
if go2rtc_config.get("ffmpeg") is None:
|
||||
go2rtc_config["ffmpeg"] = {"rtsp": rtsp_args}
|
||||
elif go2rtc_config["ffmpeg"].get("rtsp") is None:
|
||||
go2rtc_config["ffmpeg"]["rtsp"] = rtsp_args
|
||||
|
||||
for name in go2rtc_config.get("streams", {}):
|
||||
stream = go2rtc_config["streams"][name]
|
||||
|
||||
if isinstance(stream, str):
|
||||
try:
|
||||
go2rtc_config["streams"][name] = go2rtc_config["streams"][name].format(
|
||||
**FRIGATE_ENV_VARS
|
||||
)
|
||||
except KeyError as e:
|
||||
print(
|
||||
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
||||
)
|
||||
sys.exit(e)
|
||||
|
||||
elif isinstance(stream, list):
|
||||
for i, stream in enumerate(stream):
|
||||
try:
|
||||
go2rtc_config["streams"][name][i] = stream.format(**FRIGATE_ENV_VARS)
|
||||
except KeyError as e:
|
||||
print(
|
||||
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
||||
)
|
||||
sys.exit(e)
|
||||
|
||||
# add birdseye restream stream if enabled
|
||||
if config.get("birdseye", {}).get("restream", False):
|
||||
birdseye: dict[str, Any] = config.get("birdseye")
|
||||
|
||||
input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}"
|
||||
ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}"
|
||||
|
||||
if go2rtc_config.get("streams"):
|
||||
go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd
|
||||
else:
|
||||
go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd}
|
||||
|
||||
# Write go2rtc_config to multiple possible locations with proper permissions
|
||||
config_paths = [
|
||||
"/dev/shm/go2rtc.yaml", # Primary location
|
||||
"/tmp/go2rtc.yaml", # Fallback location 1
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "go2rtc.yaml") # Fallback location 2
|
||||
]
|
||||
|
||||
success = False
|
||||
for config_path in config_paths:
|
||||
try:
|
||||
# Try to create directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
|
||||
# Write to a temporary file first
|
||||
temp_path = f"{config_path}.tmp"
|
||||
with open(temp_path, "w") as f:
|
||||
yaml.dump(go2rtc_config, f)
|
||||
|
||||
# Make the file world-readable and writable
|
||||
os.chmod(temp_path, 0o666)
|
||||
|
||||
# Move the temporary file to the final location
|
||||
shutil.move(temp_path, config_path)
|
||||
|
||||
print(f"Successfully wrote go2rtc config to {config_path}")
|
||||
success = True
|
||||
break
|
||||
except (PermissionError, OSError) as e:
|
||||
print(f"Failed to write to {config_path}: {e}")
|
||||
|
||||
if not success:
|
||||
print("ERROR: Could not write go2rtc config to any location")
|
||||
sys.exit(1)
|
@ -192,6 +192,7 @@
|
||||
"edit": "Edit Camera",
|
||||
"description": "Configure camera settings including stream inputs and roles.",
|
||||
"name": "Camera Name",
|
||||
"nameOnlyChangeToFriendlyName": "This will only change the Friendly Name.",
|
||||
"nameRequired": "Camera name is required",
|
||||
"nameLength": "Camera name must be less than 24 characters.",
|
||||
"namePlaceholder": "e.g., front_door",
|
||||
@ -200,7 +201,7 @@
|
||||
"inputs": "Input Streams",
|
||||
"go2rtc": {
|
||||
"title": "Use go2rtc stream",
|
||||
"description": "will use go2rtc",
|
||||
"description": "It will utilize go2rtc, enabling a smoother live viewing experience and allowing you to hear the camera audio.",
|
||||
"compatibility": {
|
||||
"title": "Video codec compatibility (Advanced)",
|
||||
"description": "Here you can configure common go2rtc FFmpeg encoding parameters. Some of these parameters may help resolve certain video or audio issues.",
|
||||
|
@ -250,7 +250,7 @@ export default function CameraEditForm({
|
||||
if (cameraName && config?.cameras[cameraName]) {
|
||||
const camera = config.cameras[cameraName];
|
||||
defaultValues.enabled = camera.enabled ?? true;
|
||||
defaultValues.ffmpeg.inputs = camera.ffmpeg?.inputs?.length
|
||||
defaultValues.ffmpeg.inputs = camera.ffmpeg?.inputs
|
||||
? camera.ffmpeg.inputs.map((input) => {
|
||||
const isGo2rtcPath = input.path.match(
|
||||
/^rtsp:\/\/127\.0\.0\.1:8554\/(.+)$/,
|
||||
@ -263,10 +263,7 @@ export default function CameraEditForm({
|
||||
if (go2rtcStreamName && config.go2rtc?.streams) {
|
||||
Object.entries(config.go2rtc.streams).forEach(
|
||||
([streamKey, streamConfig]) => {
|
||||
if (
|
||||
streamKey === go2rtcStreamName ||
|
||||
streamKey.startsWith(`${cameraName}_`)
|
||||
) {
|
||||
if (streamKey === go2rtcStreamName) {
|
||||
if (Array.isArray(streamConfig) && streamConfig.length >= 1) {
|
||||
originalPath = streamConfig[0] || "";
|
||||
ffmpegParams = streamConfig[1] || "";
|
||||
@ -319,7 +316,9 @@ export default function CameraEditForm({
|
||||
let friendly_name: string | undefined = undefined;
|
||||
const isValidName = /^[a-zA-Z0-9_-]+$/.test(values.cameraName);
|
||||
if (!isValidName) {
|
||||
finalCameraName = generateFixedHash(finalCameraName);
|
||||
finalCameraName = cameraName
|
||||
? cameraName
|
||||
: generateFixedHash(finalCameraName);
|
||||
friendly_name = values.cameraName;
|
||||
}
|
||||
|
||||
@ -366,6 +365,8 @@ export default function CameraEditForm({
|
||||
// Add update_topic for new cameras
|
||||
if (!cameraName) {
|
||||
requestBody.update_topic = `config/cameras/${finalCameraName}/add`;
|
||||
} else {
|
||||
requestBody.update_topic = `config/cameras/${finalCameraName}/edit`;
|
||||
}
|
||||
|
||||
axios
|
||||
@ -399,51 +400,7 @@ export default function CameraEditForm({
|
||||
};
|
||||
|
||||
const onSubmit = (values: FormValues) => {
|
||||
if (cameraName && values.cameraName !== cameraName) {
|
||||
// If camera name changed, delete old camera config
|
||||
const deleteRequestBody: ConfigSetBody = {
|
||||
requires_restart: 1,
|
||||
config_data: {
|
||||
cameras: {
|
||||
[cameraName]: "",
|
||||
},
|
||||
},
|
||||
update_topic: `config/cameras/${cameraName}/remove`,
|
||||
};
|
||||
const camera = config?.cameras[cameraName];
|
||||
if (values.ffmpeg.inputs.some((input) => input.go2rtc)) {
|
||||
camera?.ffmpeg.inputs.map((input) => {
|
||||
const isGo2rtcPath = input.path.match(
|
||||
/^rtsp:\/\/127\.0\.0\.1:8554\/(.+)$/,
|
||||
);
|
||||
const go2rtcStreamName = isGo2rtcPath ? isGo2rtcPath[1] : "";
|
||||
deleteRequestBody.config_data.go2rtc = {
|
||||
streams: {
|
||||
[go2rtcStreamName]: "",
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
axios
|
||||
.put("config/set", deleteRequestBody)
|
||||
.then(() => saveCameraConfig(values))
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error.response?.data?.message ||
|
||||
error.response?.data?.detail ||
|
||||
"Unknown error";
|
||||
toast.error(
|
||||
t("toast.save.error.title", { errorMessage, ns: "common" }),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
saveCameraConfig(values);
|
||||
}
|
||||
saveCameraConfig(values);
|
||||
};
|
||||
|
||||
// Determine available roles for new streams
|
||||
@ -486,11 +443,19 @@ export default function CameraEditForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("camera.cameraConfig.name")}</FormLabel>
|
||||
{cameraName ? (
|
||||
<>
|
||||
<div className="my-3 text-sm text-muted-foreground">
|
||||
{t("camera.cameraConfig.nameOnlyChangeToFriendlyName")}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t("camera.cameraConfig.namePlaceholder")}
|
||||
{...field}
|
||||
disabled={!!cameraName} // Prevent editing name for existing cameras
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
83
web/src/components/ui/select-input.tsx
Normal file
83
web/src/components/ui/select-input.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import * as React from "react"
|
||||
import { Input, InputProps } from "./input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "./select"
|
||||
|
||||
export interface SelectInputProps extends InputProps {
|
||||
options: { value: string; label: string }[]
|
||||
onSelectChange?: (value: string) => void
|
||||
selectPlaceholder?: string
|
||||
selectClassName?: string
|
||||
selectPosition?: "before" | "after"
|
||||
}
|
||||
|
||||
const SelectInput = React.forwardRef<HTMLInputElement, SelectInputProps>(
|
||||
({
|
||||
className,
|
||||
options,
|
||||
onSelectChange,
|
||||
selectPlaceholder = "Select...",
|
||||
selectClassName = "w-[180px]",
|
||||
selectPosition = "after",
|
||||
...props
|
||||
}, ref) => {
|
||||
const [selectedValue, setSelectedValue] = React.useState<string>("")
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
setSelectedValue(value)
|
||||
if (onSelectChange) {
|
||||
onSelectChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{selectPosition === "before" && (
|
||||
<Select value={selectedValue} onValueChange={handleValueChange}>
|
||||
<SelectTrigger className={selectClassName}>
|
||||
<SelectValue placeholder={selectPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Input className={className} ref={ref} {...props} />
|
||||
|
||||
{selectPosition === "after" && (
|
||||
<Select value={selectedValue} onValueChange={handleValueChange}>
|
||||
<SelectTrigger className={selectClassName}>
|
||||
<SelectValue placeholder={selectPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SelectInput.displayName = "SelectInput"
|
||||
|
||||
export { SelectInput }
|
Loading…
Reference in New Issue
Block a user