mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Feature: camera debug/config enhancements (#6920)
* Add functionality to update YAML config file with PUT request in HTTP endpoint
* Refactor copying of text to clipboard with Clipboard API and fallback to document.execCommand('copy') in CameraMap.jsx file
* Update YAML file from URL query parameters in frigate/http.py and add functionality to save motion masks, zones, and object masks in CameraMap.jsx
* formatting
* fix merging fuckup
* Refactor camera zone coordinate saving to use single query parameter per zone in CameraMap.jsx
* remove unnecessary print statements in util.py
* Refactor update_yaml_file function to separate the logic for updating YAML data into a new function update_yaml().
* fix merge errors
* Refactor code to improve error handling and add dependencies to useEffect hooks
			
			
This commit is contained in:
		
							parent
							
								
									22cc2712a6
								
							
						
					
					
						commit
						f48dd8c1ab
					
				@ -30,7 +30,7 @@ from playhouse.sqliteq import SqliteQueueDatabase
 | 
				
			|||||||
from tzlocal import get_localzone_name
 | 
					from tzlocal import get_localzone_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from frigate.config import FrigateConfig
 | 
					from frigate.config import FrigateConfig
 | 
				
			||||||
from frigate.const import CLIPS_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
 | 
					from frigate.const import CLIPS_DIR, CONFIG_DIR, MAX_SEGMENT_DURATION, RECORD_DIR
 | 
				
			||||||
from frigate.events.external import ExternalEventProcessor
 | 
					from frigate.events.external import ExternalEventProcessor
 | 
				
			||||||
from frigate.models import Event, Recordings, Timeline
 | 
					from frigate.models import Event, Recordings, Timeline
 | 
				
			||||||
from frigate.object_processing import TrackedObject
 | 
					from frigate.object_processing import TrackedObject
 | 
				
			||||||
@ -39,7 +39,11 @@ from frigate.ptz import OnvifController
 | 
				
			|||||||
from frigate.record.export import PlaybackFactorEnum, RecordingExporter
 | 
					from frigate.record.export import PlaybackFactorEnum, RecordingExporter
 | 
				
			||||||
from frigate.stats import stats_snapshot
 | 
					from frigate.stats import stats_snapshot
 | 
				
			||||||
from frigate.storage import StorageMaintainer
 | 
					from frigate.storage import StorageMaintainer
 | 
				
			||||||
from frigate.util.builtin import clean_camera_user_pass, get_tz_modifiers
 | 
					from frigate.util.builtin import (
 | 
				
			||||||
 | 
					    clean_camera_user_pass,
 | 
				
			||||||
 | 
					    get_tz_modifiers,
 | 
				
			||||||
 | 
					    update_yaml_from_url,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from frigate.util.services import ffprobe_stream, restart_frigate, vainfo_hwaccel
 | 
					from frigate.util.services import ffprobe_stream, restart_frigate, vainfo_hwaccel
 | 
				
			||||||
from frigate.version import VERSION
 | 
					from frigate.version import VERSION
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1026,6 +1030,48 @@ def config_save():
 | 
				
			|||||||
        return "Config successfully saved.", 200
 | 
					        return "Config successfully saved.", 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@bp.route("/config/set", methods=["PUT"])
 | 
				
			||||||
 | 
					def config_set():
 | 
				
			||||||
 | 
					    config_file = os.environ.get("CONFIG_FILE", f"{CONFIG_DIR}/config.yml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check if we can use .yaml instead of .yml
 | 
				
			||||||
 | 
					    config_file_yaml = config_file.replace(".yml", ".yaml")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if os.path.isfile(config_file_yaml):
 | 
				
			||||||
 | 
					        config_file = config_file_yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open(config_file, "r") as f:
 | 
				
			||||||
 | 
					        old_raw_config = f.read()
 | 
				
			||||||
 | 
					        f.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        update_yaml_from_url(config_file, request.url)
 | 
				
			||||||
 | 
					        with open(config_file, "r") as f:
 | 
				
			||||||
 | 
					            new_raw_config = f.read()
 | 
				
			||||||
 | 
					            f.close()
 | 
				
			||||||
 | 
					        # Validate the config schema
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            FrigateConfig.parse_raw(new_raw_config)
 | 
				
			||||||
 | 
					        except Exception:
 | 
				
			||||||
 | 
					            with open(config_file, "w") as f:
 | 
				
			||||||
 | 
					                f.write(old_raw_config)
 | 
				
			||||||
 | 
					                f.close()
 | 
				
			||||||
 | 
					            return make_response(
 | 
				
			||||||
 | 
					                jsonify(
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "success": False,
 | 
				
			||||||
 | 
					                        "message": f"\nConfig Error:\n\n{str(traceback.format_exc())}",
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                400,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        logging.error(f"Error updating config: {e}")
 | 
				
			||||||
 | 
					        return "Error updating config", 500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return "Config successfully updated", 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@bp.route("/config/schema.json")
 | 
					@bp.route("/config/schema.json")
 | 
				
			||||||
def config_schema():
 | 
					def config_schema():
 | 
				
			||||||
    return current_app.response_class(
 | 
					    return current_app.response_class(
 | 
				
			||||||
 | 
				
			|||||||
@ -14,10 +14,12 @@ from collections.abc import Mapping
 | 
				
			|||||||
from queue import Empty, Full
 | 
					from queue import Empty, Full
 | 
				
			||||||
from typing import Any, Tuple
 | 
					from typing import Any, Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import numpy as np
 | 
				
			||||||
import pytz
 | 
					import pytz
 | 
				
			||||||
import yaml
 | 
					import yaml
 | 
				
			||||||
from faster_fifo import DEFAULT_CIRCULAR_BUFFER_SIZE, DEFAULT_TIMEOUT
 | 
					from faster_fifo import DEFAULT_CIRCULAR_BUFFER_SIZE, DEFAULT_TIMEOUT
 | 
				
			||||||
from faster_fifo import Queue as FFQueue
 | 
					from faster_fifo import Queue as FFQueue
 | 
				
			||||||
 | 
					from ruamel.yaml import YAML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
 | 
					from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -224,3 +226,76 @@ def to_relative_box(
 | 
				
			|||||||
        (box[2] - box[0]) / width,  # w
 | 
					        (box[2] - box[0]) / width,  # w
 | 
				
			||||||
        (box[3] - box[1]) / height,  # h
 | 
					        (box[3] - box[1]) / height,  # h
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_mask(frame_shape, mask):
 | 
				
			||||||
 | 
					    mask_img = np.zeros(frame_shape, np.uint8)
 | 
				
			||||||
 | 
					    mask_img[:] = 255
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_yaml_from_url(file_path, url):
 | 
				
			||||||
 | 
					    parsed_url = urllib.parse.urlparse(url)
 | 
				
			||||||
 | 
					    query_string = urllib.parse.parse_qs(parsed_url.query, keep_blank_values=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for key_path_str, new_value_list in query_string.items():
 | 
				
			||||||
 | 
					        key_path = key_path_str.split(".")
 | 
				
			||||||
 | 
					        for i in range(len(key_path)):
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                index = int(key_path[i])
 | 
				
			||||||
 | 
					                key_path[i] = (key_path[i - 1], index)
 | 
				
			||||||
 | 
					                key_path.pop(i - 1)
 | 
				
			||||||
 | 
					            except ValueError:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					        new_value = new_value_list[0]
 | 
				
			||||||
 | 
					        update_yaml_file(file_path, key_path, new_value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_yaml_file(file_path, key_path, new_value):
 | 
				
			||||||
 | 
					    yaml = YAML()
 | 
				
			||||||
 | 
					    with open(file_path, "r") as f:
 | 
				
			||||||
 | 
					        data = yaml.load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    data = update_yaml(data, key_path, new_value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open(file_path, "w") as f:
 | 
				
			||||||
 | 
					        yaml.dump(data, f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_yaml(data, key_path, new_value):
 | 
				
			||||||
 | 
					    temp = data
 | 
				
			||||||
 | 
					    for key in key_path[:-1]:
 | 
				
			||||||
 | 
					        if isinstance(key, tuple):
 | 
				
			||||||
 | 
					            if key[0] not in temp:
 | 
				
			||||||
 | 
					                temp[key[0]] = [{}] * max(1, key[1] + 1)
 | 
				
			||||||
 | 
					            elif len(temp[key[0]]) <= key[1]:
 | 
				
			||||||
 | 
					                temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1)
 | 
				
			||||||
 | 
					            temp = temp[key[0]][key[1]]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if key not in temp:
 | 
				
			||||||
 | 
					                temp[key] = {}
 | 
				
			||||||
 | 
					            temp = temp[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    last_key = key_path[-1]
 | 
				
			||||||
 | 
					    if new_value == "":
 | 
				
			||||||
 | 
					        if isinstance(last_key, tuple):
 | 
				
			||||||
 | 
					            del temp[last_key[0]][last_key[1]]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            del temp[last_key]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        if isinstance(last_key, tuple):
 | 
				
			||||||
 | 
					            if last_key[0] not in temp:
 | 
				
			||||||
 | 
					                temp[last_key[0]] = [{}] * max(1, last_key[1] + 1)
 | 
				
			||||||
 | 
					            elif len(temp[last_key[0]]) <= last_key[1]:
 | 
				
			||||||
 | 
					                temp[last_key[0]] += [{}] * (last_key[1] - len(temp[last_key[0]]) + 1)
 | 
				
			||||||
 | 
					            temp[last_key[0]][last_key[1]] = new_value
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                last_key in temp
 | 
				
			||||||
 | 
					                and isinstance(temp[last_key], dict)
 | 
				
			||||||
 | 
					                and isinstance(new_value, dict)
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
 | 
					                temp[last_key].update(new_value)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                temp[last_key] = new_value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ pydantic == 1.10.*
 | 
				
			|||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
 | 
					git+https://github.com/fbcotter/py3nvml#egg=py3nvml
 | 
				
			||||||
PyYAML == 6.0
 | 
					PyYAML == 6.0
 | 
				
			||||||
pytz == 2023.3
 | 
					pytz == 2023.3
 | 
				
			||||||
 | 
					ruamel.yaml == 0.17.*
 | 
				
			||||||
tzlocal == 5.0.*
 | 
					tzlocal == 5.0.*
 | 
				
			||||||
types-PyYAML == 6.0.*
 | 
					types-PyYAML == 6.0.*
 | 
				
			||||||
requests == 2.31.*
 | 
					requests == 2.31.*
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ import { useResizeObserver } from '../hooks';
 | 
				
			|||||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
 | 
					import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
 | 
				
			||||||
import { useApiHost } from '../api';
 | 
					import { useApiHost } from '../api';
 | 
				
			||||||
import useSWR from 'swr';
 | 
					import useSWR from 'swr';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
export default function CameraMasks({ camera }) {
 | 
					export default function CameraMasks({ camera }) {
 | 
				
			||||||
  const { data: config } = useSWR('config');
 | 
					  const { data: config } = useSWR('config');
 | 
				
			||||||
  const apiHost = useApiHost();
 | 
					  const apiHost = useApiHost();
 | 
				
			||||||
@ -95,12 +95,53 @@ export default function CameraMasks({ camera }) {
 | 
				
			|||||||
    [motionMaskPoints, setMotionMaskPoints]
 | 
					    [motionMaskPoints, setMotionMaskPoints]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleCopyMotionMasks = useCallback(async () => {
 | 
					  const handleCopyMotionMasks = useCallback(() => {
 | 
				
			||||||
    await window.navigator.clipboard.writeText(`  motion:
 | 
					    const textToCopy = `  motion:
 | 
				
			||||||
    mask:
 | 
					      mask:
 | 
				
			||||||
${motionMaskPoints.map((mask) => `      - ${polylinePointsToPolyline(mask)}`).join('\n')}`);
 | 
					  ${motionMaskPoints.map((mask) => `      - ${polylinePointsToPolyline(mask)}`).join('\n')}`;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					    if (window.navigator.clipboard && window.navigator.clipboard.writeText) {
 | 
				
			||||||
 | 
					      // Use Clipboard API if available
 | 
				
			||||||
 | 
					      window.navigator.clipboard.writeText(textToCopy).catch((err) => {
 | 
				
			||||||
 | 
					        throw new Error('Failed to copy text: ', err);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Fallback to document.execCommand('copy')
 | 
				
			||||||
 | 
					      const textarea = document.createElement('textarea');
 | 
				
			||||||
 | 
					      textarea.value = textToCopy;
 | 
				
			||||||
 | 
					      document.body.appendChild(textarea);
 | 
				
			||||||
 | 
					      textarea.select();
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const successful = document.execCommand('copy');
 | 
				
			||||||
 | 
					        if (!successful) {
 | 
				
			||||||
 | 
					          throw new Error('Failed to copy text');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to copy text: ', err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					      document.body.removeChild(textarea);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, [motionMaskPoints]);
 | 
					  }, [motionMaskPoints]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSaveMotionMasks = useCallback(async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const queryParameters = motionMaskPoints
 | 
				
			||||||
 | 
					        .map((mask, index) => `cameras.${camera}.motion.mask.${index}=${polylinePointsToPolyline(mask)}`)
 | 
				
			||||||
 | 
					        .join('&');
 | 
				
			||||||
 | 
					      const endpoint = `config/set?${queryParameters}`;
 | 
				
			||||||
 | 
					      const response = await axios.put(endpoint);
 | 
				
			||||||
 | 
					      if (response.status === 200) {
 | 
				
			||||||
 | 
					        // handle successful response
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // handle error
 | 
				
			||||||
 | 
					      //console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [camera, motionMaskPoints]);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Zone methods
 | 
					  // Zone methods
 | 
				
			||||||
  const handleEditZone = useCallback(
 | 
					  const handleEditZone = useCallback(
 | 
				
			||||||
    (key) => {
 | 
					    (key) => {
 | 
				
			||||||
@ -127,15 +168,53 @@ ${motionMaskPoints.map((mask) => `      - ${polylinePointsToPolyline(mask)}`).jo
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleCopyZones = useCallback(async () => {
 | 
					  const handleCopyZones = useCallback(async () => {
 | 
				
			||||||
    await window.navigator.clipboard.writeText(`  zones:
 | 
					    const textToCopy = `  zones:
 | 
				
			||||||
${Object.keys(zonePoints)
 | 
					${Object.keys(zonePoints)
 | 
				
			||||||
    .map(
 | 
					    .map(
 | 
				
			||||||
      (zoneName) => `    ${zoneName}:
 | 
					      (zoneName) => `    ${zoneName}:
 | 
				
			||||||
      coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
 | 
					      coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`).join('\n')}`;
 | 
				
			||||||
    )
 | 
					
 | 
				
			||||||
    .join('\n')}`);
 | 
					    if (window.navigator.clipboard && window.navigator.clipboard.writeText) {
 | 
				
			||||||
 | 
					      // Use Clipboard API if available
 | 
				
			||||||
 | 
					      window.navigator.clipboard.writeText(textToCopy).catch((err) => {
 | 
				
			||||||
 | 
					        throw new Error('Failed to copy text: ', err);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Fallback to document.execCommand('copy')
 | 
				
			||||||
 | 
					      const textarea = document.createElement('textarea');
 | 
				
			||||||
 | 
					      textarea.value = textToCopy;
 | 
				
			||||||
 | 
					      document.body.appendChild(textarea);
 | 
				
			||||||
 | 
					      textarea.select();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const successful = document.execCommand('copy');
 | 
				
			||||||
 | 
					        if (!successful) {
 | 
				
			||||||
 | 
					          throw new Error('Failed to copy text');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to copy text: ', err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					      document.body.removeChild(textarea);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, [zonePoints]);
 | 
					  }, [zonePoints]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSaveZones = useCallback(async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const queryParameters = Object.keys(zonePoints)
 | 
				
			||||||
 | 
					        .map((zoneName) => `cameras.${camera}.zones.${zoneName}.coordinates=${polylinePointsToPolyline(zonePoints[zoneName])}`)
 | 
				
			||||||
 | 
					        .join('&');
 | 
				
			||||||
 | 
					      const endpoint = `config/set?${queryParameters}`;
 | 
				
			||||||
 | 
					      const response = await axios.put(endpoint);
 | 
				
			||||||
 | 
					      if (response.status === 200) {
 | 
				
			||||||
 | 
					        // handle successful response
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // handle error
 | 
				
			||||||
 | 
					      //console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [camera, zonePoints]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Object methods
 | 
					  // Object methods
 | 
				
			||||||
  const handleEditObjectMask = useCallback(
 | 
					  const handleEditObjectMask = useCallback(
 | 
				
			||||||
    (key, subkey) => {
 | 
					    (key, subkey) => {
 | 
				
			||||||
@ -175,6 +254,23 @@ ${Object.keys(objectMaskPoints)
 | 
				
			|||||||
    .join('\n')}`);
 | 
					    .join('\n')}`);
 | 
				
			||||||
  }, [objectMaskPoints]);
 | 
					  }, [objectMaskPoints]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSaveObjectMasks = useCallback(async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const queryParameters = Object.keys(objectMaskPoints)
 | 
				
			||||||
 | 
					        .filter((objectName) => objectMaskPoints[objectName].length > 0)
 | 
				
			||||||
 | 
					        .map((objectName, index) => `cameras.${camera}.objects.filters.${objectName}.mask.${index}=${polylinePointsToPolyline(objectMaskPoints[objectName])}`)
 | 
				
			||||||
 | 
					        .join('&');
 | 
				
			||||||
 | 
					      const endpoint = `config/set?${queryParameters}`;
 | 
				
			||||||
 | 
					      const response = await axios.put(endpoint);
 | 
				
			||||||
 | 
					      if (response.status === 200) {
 | 
				
			||||||
 | 
					        // handle successful response
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      // handle error
 | 
				
			||||||
 | 
					      //console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [camera, objectMaskPoints]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddToObjectMask = useCallback(
 | 
					  const handleAddToObjectMask = useCallback(
 | 
				
			||||||
    (key) => {
 | 
					    (key) => {
 | 
				
			||||||
      const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
 | 
					      const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
 | 
				
			||||||
@ -246,6 +342,7 @@ ${Object.keys(objectMaskPoints)
 | 
				
			|||||||
          editing={editing}
 | 
					          editing={editing}
 | 
				
			||||||
          title="Motion masks"
 | 
					          title="Motion masks"
 | 
				
			||||||
          onCopy={handleCopyMotionMasks}
 | 
					          onCopy={handleCopyMotionMasks}
 | 
				
			||||||
 | 
					          onSave={handleSaveMotionMasks}
 | 
				
			||||||
          onCreate={handleAddMask}
 | 
					          onCreate={handleAddMask}
 | 
				
			||||||
          onEdit={handleEditMask}
 | 
					          onEdit={handleEditMask}
 | 
				
			||||||
          onRemove={handleRemoveMask}
 | 
					          onRemove={handleRemoveMask}
 | 
				
			||||||
@ -258,6 +355,7 @@ ${Object.keys(objectMaskPoints)
 | 
				
			|||||||
          editing={editing}
 | 
					          editing={editing}
 | 
				
			||||||
          title="Zones"
 | 
					          title="Zones"
 | 
				
			||||||
          onCopy={handleCopyZones}
 | 
					          onCopy={handleCopyZones}
 | 
				
			||||||
 | 
					          onSave={handleSaveZones}
 | 
				
			||||||
          onCreate={handleAddZone}
 | 
					          onCreate={handleAddZone}
 | 
				
			||||||
          onEdit={handleEditZone}
 | 
					          onEdit={handleEditZone}
 | 
				
			||||||
          onRemove={handleRemoveZone}
 | 
					          onRemove={handleRemoveZone}
 | 
				
			||||||
@ -272,6 +370,7 @@ ${Object.keys(objectMaskPoints)
 | 
				
			|||||||
          title="Object masks"
 | 
					          title="Object masks"
 | 
				
			||||||
          onAdd={handleAddToObjectMask}
 | 
					          onAdd={handleAddToObjectMask}
 | 
				
			||||||
          onCopy={handleCopyObjectMasks}
 | 
					          onCopy={handleCopyObjectMasks}
 | 
				
			||||||
 | 
					          onSave={handleSaveObjectMasks}
 | 
				
			||||||
          onCreate={handleAddObjectMask}
 | 
					          onCreate={handleAddObjectMask}
 | 
				
			||||||
          onEdit={handleEditObjectMask}
 | 
					          onEdit={handleEditObjectMask}
 | 
				
			||||||
          onRemove={handleRemoveObjectMask}
 | 
					          onRemove={handleRemoveObjectMask}
 | 
				
			||||||
@ -407,6 +506,7 @@ function MaskValues({
 | 
				
			|||||||
  title,
 | 
					  title,
 | 
				
			||||||
  onAdd,
 | 
					  onAdd,
 | 
				
			||||||
  onCopy,
 | 
					  onCopy,
 | 
				
			||||||
 | 
					  onSave,
 | 
				
			||||||
  onCreate,
 | 
					  onCreate,
 | 
				
			||||||
  onEdit,
 | 
					  onEdit,
 | 
				
			||||||
  onRemove,
 | 
					  onRemove,
 | 
				
			||||||
@ -455,6 +555,8 @@ function MaskValues({
 | 
				
			|||||||
    [onAdd]
 | 
					    [onAdd]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
 | 
					    <div className="overflow-hidden" onMouseOver={handleMousein} onMouseOut={handleMouseout}>
 | 
				
			||||||
      <div className="flex space-x-4">
 | 
					      <div className="flex space-x-4">
 | 
				
			||||||
@ -463,6 +565,7 @@ function MaskValues({
 | 
				
			|||||||
        </Heading>
 | 
					        </Heading>
 | 
				
			||||||
        <Button onClick={onCopy}>Copy</Button>
 | 
					        <Button onClick={onCopy}>Copy</Button>
 | 
				
			||||||
        <Button onClick={onCreate}>Add</Button>
 | 
					        <Button onClick={onCreate}>Add</Button>
 | 
				
			||||||
 | 
					        <Button onClick={onSave}>Save</Button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <pre className="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
 | 
					      <pre className="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
 | 
				
			||||||
        {yamlPrefix}
 | 
					        {yamlPrefix}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user