From 85741c02e47f6ac9d33dd6308d31cfb2e76ca796 Mon Sep 17 00:00:00 2001 From: ZhaiSoul <842607283@qq.com> Date: Fri, 29 Aug 2025 09:05:55 +0000 Subject: [PATCH] feat: reload go2rtc config --- frigate/api/app.py | 49 +++- frigate/camera/maintainer.py | 21 +- frigate/config/camera/updater.py | 219 ++++++++++++++++-- frigate/embeddings/maintainer.py | 1 + scripts/create_go2rtc_config.py | 216 +++++++++++++++++ web/public/locales/en/views/settings.json | 3 +- .../components/settings/CameraEditForm.tsx | 69 ++---- web/src/components/ui/select-input.tsx | 83 +++++++ 8 files changed, 577 insertions(+), 84 deletions(-) create mode 100644 scripts/create_go2rtc_config.py create mode 100644 web/src/components/ui/select-input.tsx diff --git a/frigate/api/app.py b/frigate/api/app.py index d9e573d29..bbc83a356 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -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"]), diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index 865fe4725..cd9fb0a15 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -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(): diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py index 630961de2..26e19381d 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -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}") diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index a129b9677..91d6d9c97 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -102,6 +102,7 @@ class EmbeddingMaintainer(threading.Thread): [ CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.edit, CameraConfigUpdateEnum.object_genai, CameraConfigUpdateEnum.review_genai, CameraConfigUpdateEnum.semantic_search, diff --git a/scripts/create_go2rtc_config.py b/scripts/create_go2rtc_config.py new file mode 100644 index 000000000..0f1b6a08f --- /dev/null +++ b/scripts/create_go2rtc_config.py @@ -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) \ No newline at end of file diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index b3ce0bc99..5b600580b 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -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.", diff --git a/web/src/components/settings/CameraEditForm.tsx b/web/src/components/settings/CameraEditForm.tsx index b9c2b5ffb..f7e8a32af 100644 --- a/web/src/components/settings/CameraEditForm.tsx +++ b/web/src/components/settings/CameraEditForm.tsx @@ -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 }) => ( {t("camera.cameraConfig.name")} + {cameraName ? ( + <> +
+ {t("camera.cameraConfig.nameOnlyChangeToFriendlyName")} +
+ + ) : ( + "" + )} diff --git a/web/src/components/ui/select-input.tsx b/web/src/components/ui/select-input.tsx new file mode 100644 index 000000000..c581addfe --- /dev/null +++ b/web/src/components/ui/select-input.tsx @@ -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( + ({ + className, + options, + onSelectChange, + selectPlaceholder = "Select...", + selectClassName = "w-[180px]", + selectPosition = "after", + ...props + }, ref) => { + const [selectedValue, setSelectedValue] = React.useState("") + + const handleValueChange = (value: string) => { + setSelectedValue(value) + if (onSelectChange) { + onSelectChange(value) + } + } + + return ( +
+ {selectPosition === "before" && ( + + )} + + + + {selectPosition === "after" && ( + + )} +
+ ) + } +) + +SelectInput.displayName = "SelectInput" + +export { SelectInput } \ No newline at end of file