blakeblackshear.frigate/frigate/util/config.py

421 lines
15 KiB
Python
Raw Normal View History

"""configuration utils."""
import asyncio
import logging
import os
import shutil
from typing import Optional, Union
from ruamel.yaml import YAML
from frigate.const import CONFIG_DIR, EXPORT_DIR
from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__)
Improve live streaming (#16447) * config file changes * config migrator * stream selection on single camera live view * camera streaming settings dialog * manage persistent group streaming settings * apply streaming settings in camera groups * add ability to clear all streaming settings from settings * docs * update reference config * fixes * clarify docs * use first stream as default in dialog * ensure still image is visible after switching stream type to none * docs * clarify docs * add ability to continue playing stream in background * fix props * put stream selection inside dropdown on desktop * add capabilities to live mode hook * live context menu component * resize observer: only return new dimensions if they've actually changed * pass volume prop to players * fix slider bug, https://github.com/shadcn-ui/ui/issues/1448 * update react-grid-layout * prevent animated transitions on draggable grid layout * add context menu to dashboards * use provider * streaming dialog from context menu * docs * add jsmpeg warning to context menu * audio and two way talk indicators in single camera view * add link to debug view * don't use hook * create manual events from live camera view * maintain grow classes on grid items * fix initial volume state on default dashboard * fix pointer events causing context menu to end up underneath image on iOS * mobile drawer tweaks * stream stats * show settings menu for non-restreamed cameras * consistent settings icon * tweaks * optional stats to fix birdseye player * add toaster to live camera view * fix crash on initial save in streaming dialog * don't require restreaming for context menu streaming settings * add debug view to context menu * stats fixes * update docs * always show stream info when restreamed * update camera streaming dialog * make note of no h265 support for webrtc * docs clarity * ensure docs show streams as a dict * docs clarity * fix css file * tweaks
2025-02-10 17:42:35 +01:00
CURRENT_CONFIG_VERSION = "0.16-0"
DEFAULT_CONFIG_FILE = "/config/config.yml"
def find_config_file() -> str:
config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE)
if not os.path.isfile(config_path):
config_path = config_path.replace("yml", "yaml")
return config_path
def migrate_frigate_config(config_file: str):
"""handle migrating the frigate config."""
logger.info("Checking if frigate config needs migration...")
if not os.access(config_file, mode=os.W_OK):
logger.error("Config file is read-only, unable to migrate config file.")
return
yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f:
config: dict[str, dict[str, any]] = yaml.load(f)
if config is None:
logger.error(f"Failed to load config at {config_file}")
return
previous_version = str(config.get("version", "0.13"))
if previous_version == CURRENT_CONFIG_VERSION:
logger.info("frigate config does not need migration...")
return
logger.info("copying config as backup...")
shutil.copy(config_file, os.path.join(CONFIG_DIR, "backup_config.yaml"))
if previous_version < "0.14":
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
new_config = migrate_014(config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
previous_version = "0.14"
logger.info("Migrating export file names...")
if os.path.isdir(EXPORT_DIR):
for file in os.listdir(EXPORT_DIR):
if "@" not in file:
continue
new_name = file.replace("@", "_")
os.rename(
os.path.join(EXPORT_DIR, file), os.path.join(EXPORT_DIR, new_name)
)
if previous_version < "0.15-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...")
new_config = migrate_015_0(config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
previous_version = "0.15-0"
if previous_version < "0.15-1":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...")
new_config = migrate_015_1(config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
previous_version = "0.15-1"
Improve live streaming (#16447) * config file changes * config migrator * stream selection on single camera live view * camera streaming settings dialog * manage persistent group streaming settings * apply streaming settings in camera groups * add ability to clear all streaming settings from settings * docs * update reference config * fixes * clarify docs * use first stream as default in dialog * ensure still image is visible after switching stream type to none * docs * clarify docs * add ability to continue playing stream in background * fix props * put stream selection inside dropdown on desktop * add capabilities to live mode hook * live context menu component * resize observer: only return new dimensions if they've actually changed * pass volume prop to players * fix slider bug, https://github.com/shadcn-ui/ui/issues/1448 * update react-grid-layout * prevent animated transitions on draggable grid layout * add context menu to dashboards * use provider * streaming dialog from context menu * docs * add jsmpeg warning to context menu * audio and two way talk indicators in single camera view * add link to debug view * don't use hook * create manual events from live camera view * maintain grow classes on grid items * fix initial volume state on default dashboard * fix pointer events causing context menu to end up underneath image on iOS * mobile drawer tweaks * stream stats * show settings menu for non-restreamed cameras * consistent settings icon * tweaks * optional stats to fix birdseye player * add toaster to live camera view * fix crash on initial save in streaming dialog * don't require restreaming for context menu streaming settings * add debug view to context menu * stats fixes * update docs * always show stream info when restreamed * update camera streaming dialog * make note of no h265 support for webrtc * docs clarity * ensure docs show streams as a dict * docs clarity * fix css file * tweaks
2025-02-10 17:42:35 +01:00
if previous_version < "0.16-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...")
new_config = migrate_016_0(config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
previous_version = "0.16-0"
logger.info("Finished frigate config migration...")
def migrate_014(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
"""Handle migrating frigate config to 0.14"""
# migrate record.events.required_zones to review.alerts.required_zones
new_config = config.copy()
global_required_zones = (
config.get("record", {}).get("events", {}).get("required_zones", [])
)
if global_required_zones:
# migrate to new review config
if not new_config.get("review"):
new_config["review"] = {}
if not new_config["review"].get("alerts"):
new_config["review"]["alerts"] = {}
if not new_config["review"]["alerts"].get("required_zones"):
new_config["review"]["alerts"]["required_zones"] = global_required_zones
# remove record required zones config
del new_config["record"]["events"]["required_zones"]
# remove record altogether if there is not other config
if not new_config["record"]["events"]:
del new_config["record"]["events"]
if not new_config["record"]:
del new_config["record"]
2024-07-16 15:45:11 +02:00
# Remove UI fields
if new_config.get("ui"):
if new_config["ui"].get("use_experimental"):
2024-08-09 23:59:55 +02:00
del new_config["ui"]["use_experimental"]
2024-07-16 15:45:11 +02:00
if new_config["ui"].get("live_mode"):
del new_config["ui"]["live_mode"]
2024-07-16 15:45:11 +02:00
if not new_config["ui"]:
del new_config["ui"]
# remove rtmp
if new_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"):
del new_config["ffmpeg"]["output_args"]["rtmp"]
if new_config.get("rtmp"):
del new_config["rtmp"]
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
required_zones = (
camera_config.get("record", {}).get("events", {}).get("required_zones", [])
)
if required_zones:
# migrate to new review config
if not camera_config.get("review"):
camera_config["review"] = {}
if not camera_config["review"].get("alerts"):
camera_config["review"]["alerts"] = {}
if not camera_config["review"]["alerts"].get("required_zones"):
camera_config["review"]["alerts"]["required_zones"] = required_zones
# remove record required zones config
del camera_config["record"]["events"]["required_zones"]
# remove record altogether if there is not other config
if not camera_config["record"]["events"]:
del camera_config["record"]["events"]
if not camera_config["record"]:
del camera_config["record"]
# remove rtmp
if camera_config.get("ffmpeg", {}).get("output_args", {}).get("rtmp"):
del camera_config["ffmpeg"]["output_args"]["rtmp"]
if camera_config.get("rtmp"):
del camera_config["rtmp"]
new_config["cameras"][name] = camera_config
new_config["version"] = "0.14"
return new_config
def migrate_015_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
"""Handle migrating frigate config to 0.15-0"""
new_config = config.copy()
# migrate record.events to record.alerts and record.detections
global_record_events = config.get("record", {}).get("events")
if global_record_events:
alerts_retention = {"retain": {}}
detections_retention = {"retain": {}}
if global_record_events.get("pre_capture"):
alerts_retention["pre_capture"] = global_record_events["pre_capture"]
if global_record_events.get("post_capture"):
alerts_retention["post_capture"] = global_record_events["post_capture"]
if global_record_events.get("retain", {}).get("default"):
alerts_retention["retain"]["days"] = global_record_events["retain"][
"default"
]
# decide logical detections retention based on current detections config
if not config.get("review", {}).get("alerts", {}).get(
"required_zones"
) or config.get("review", {}).get("detections"):
if global_record_events.get("pre_capture"):
detections_retention["pre_capture"] = global_record_events[
"pre_capture"
]
if global_record_events.get("post_capture"):
detections_retention["post_capture"] = global_record_events[
"post_capture"
]
if global_record_events.get("retain", {}).get("default"):
detections_retention["retain"]["days"] = global_record_events["retain"][
"default"
]
else:
continuous_days = config.get("record", {}).get("retain", {}).get("days")
detections_retention["retain"]["days"] = (
continuous_days if continuous_days else 1
)
new_config["record"]["alerts"] = alerts_retention
new_config["record"]["detections"] = detections_retention
del new_config["record"]["events"]
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
record_events: dict[str, any] = camera_config.get("record", {}).get("events")
if record_events:
alerts_retention = {"retain": {}}
detections_retention = {"retain": {}}
if record_events.get("pre_capture"):
alerts_retention["pre_capture"] = record_events["pre_capture"]
if record_events.get("post_capture"):
alerts_retention["post_capture"] = record_events["post_capture"]
if record_events.get("retain", {}).get("default"):
alerts_retention["retain"]["days"] = record_events["retain"]["default"]
# decide logical detections retention based on current detections config
if not camera_config.get("review", {}).get("alerts", {}).get(
"required_zones"
) or camera_config.get("review", {}).get("detections"):
if record_events.get("pre_capture"):
detections_retention["pre_capture"] = record_events["pre_capture"]
if record_events.get("post_capture"):
detections_retention["post_capture"] = record_events["post_capture"]
if record_events.get("retain", {}).get("default"):
detections_retention["retain"]["days"] = record_events["retain"][
"default"
]
else:
continuous_days = (
camera_config.get("record", {}).get("retain", {}).get("days")
)
detections_retention["retain"]["days"] = (
continuous_days if continuous_days else 1
)
camera_config["record"]["alerts"] = alerts_retention
camera_config["record"]["detections"] = detections_retention
del camera_config["record"]["events"]
new_config["cameras"][name] = camera_config
new_config["version"] = "0.15-0"
return new_config
def migrate_015_1(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
"""Handle migrating frigate config to 0.15-1"""
new_config = config.copy()
for detector, detector_config in config.get("detectors", {}).items():
path = detector_config.get("model", {}).get("path")
if path:
new_config["detectors"][detector]["model_path"] = path
del new_config["detectors"][detector]["model"]
new_config["version"] = "0.15-1"
return new_config
Improve live streaming (#16447) * config file changes * config migrator * stream selection on single camera live view * camera streaming settings dialog * manage persistent group streaming settings * apply streaming settings in camera groups * add ability to clear all streaming settings from settings * docs * update reference config * fixes * clarify docs * use first stream as default in dialog * ensure still image is visible after switching stream type to none * docs * clarify docs * add ability to continue playing stream in background * fix props * put stream selection inside dropdown on desktop * add capabilities to live mode hook * live context menu component * resize observer: only return new dimensions if they've actually changed * pass volume prop to players * fix slider bug, https://github.com/shadcn-ui/ui/issues/1448 * update react-grid-layout * prevent animated transitions on draggable grid layout * add context menu to dashboards * use provider * streaming dialog from context menu * docs * add jsmpeg warning to context menu * audio and two way talk indicators in single camera view * add link to debug view * don't use hook * create manual events from live camera view * maintain grow classes on grid items * fix initial volume state on default dashboard * fix pointer events causing context menu to end up underneath image on iOS * mobile drawer tweaks * stream stats * show settings menu for non-restreamed cameras * consistent settings icon * tweaks * optional stats to fix birdseye player * add toaster to live camera view * fix crash on initial save in streaming dialog * don't require restreaming for context menu streaming settings * add debug view to context menu * stats fixes * update docs * always show stream info when restreamed * update camera streaming dialog * make note of no h265 support for webrtc * docs clarity * ensure docs show streams as a dict * docs clarity * fix css file * tweaks
2025-02-10 17:42:35 +01:00
def migrate_016_0(config: dict[str, dict[str, any]]) -> dict[str, dict[str, any]]:
"""Handle migrating frigate config to 0.16-0"""
new_config = config.copy()
for name, camera in config.get("cameras", {}).items():
camera_config: dict[str, dict[str, any]] = camera.copy()
live_config = camera_config.get("live", {})
if "stream_name" in live_config:
# Migrate from live -> stream_name to live -> streams -> dict
stream_name = live_config["stream_name"]
live_config["streams"] = {stream_name: stream_name}
del live_config["stream_name"]
camera_config["live"] = live_config
new_config["cameras"][name] = camera_config
new_config["version"] = "0.16-0"
return new_config
def get_relative_coordinates(
mask: Optional[Union[str, list]], frame_shape: tuple[int, int]
) -> Union[str, list]:
# masks and zones are saved as relative coordinates
# we know if any points are > 1 then it is using the
# old native resolution coordinates
if mask:
if isinstance(mask, list) and any(x > "1.0" for x in mask[0].split(",")):
relative_masks = []
for m in mask:
points = m.split(",")
2024-04-29 17:58:53 +02:00
if any(x > "1.0" for x in points):
rel_points = []
for i in range(0, len(points), 2):
x = int(points[i])
y = int(points[i + 1])
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
continue
rel_points.append(
f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}"
2024-04-29 17:58:53 +02:00
)
relative_masks.append(",".join(rel_points))
2024-04-29 17:58:53 +02:00
else:
relative_masks.append(m)
mask = relative_masks
elif isinstance(mask, str) and any(x > "1.0" for x in mask.split(",")):
points = mask.split(",")
rel_points = []
for i in range(0, len(points), 2):
x = int(points[i])
y = int(points[i + 1])
if x > frame_shape[1] or y > frame_shape[0]:
logger.error(
f"Not applying mask due to invalid coordinates. {x},{y} is outside of the detection resolution {frame_shape[1]}x{frame_shape[0]}. Use the editor in the UI to correct the mask."
)
return []
rel_points.append(
f"{round(x / frame_shape[1], 3)},{round(y / frame_shape[0], 3)}"
)
mask = ",".join(rel_points)
return mask
return mask
def convert_area_to_pixels(
area_value: Union[int, float], frame_shape: tuple[int, int]
) -> int:
"""
Convert area specification to pixels.
Args:
area_value: Area value (pixels or percentage)
frame_shape: Tuple of (height, width) for the frame
Returns:
Area in pixels
"""
# If already an integer, assume it's in pixels
if isinstance(area_value, int):
return area_value
# Check if it's a percentage
if isinstance(area_value, float):
if 0.000001 <= area_value <= 0.99:
frame_area = frame_shape[0] * frame_shape[1]
return max(1, int(frame_area * area_value))
else:
raise ValueError(
f"Percentage must be between 0.000001 and 0.99, got {area_value}"
)
raise TypeError(f"Unexpected type for area: {type(area_value)}")
class StreamInfoRetriever:
def __init__(self) -> None:
self.stream_cache: dict[str, tuple[int, int]] = {}
def get_stream_info(self, ffmpeg, path: str) -> str:
if path in self.stream_cache:
return self.stream_cache[path]
info = asyncio.run(get_video_properties(ffmpeg, path))
self.stream_cache[path] = info
return info