import { h } from 'preact'; import Card from '../components/Card.jsx'; import Button from '../components/Button.jsx'; import Heading from '../components/Heading.jsx'; import Switch from '../components/Switch.jsx'; import { useResizeObserver } from '../hooks'; import { useCallback, useMemo, useRef, useState } from 'preact/hooks'; import { useApiHost } from '../api'; import useSWR from 'swr'; import axios from 'axios'; export default function CameraMasks({ camera }) { const { data: config } = useSWR('config'); const apiHost = useApiHost(); const imageRef = useRef(null); const [snap, setSnap] = useState(true); const cameraConfig = config.cameras[camera]; const { motion: { mask: motionMask }, objects: { filters: objectFilters }, zones, } = cameraConfig; const { width, height } = cameraConfig.detect; const [{ width: scaledWidth }] = useResizeObserver(imageRef); const imageScale = scaledWidth / width; const [motionMaskPoints, setMotionMaskPoints] = useState( Array.isArray(motionMask) ? motionMask.map((mask) => getPolylinePoints(mask)) : motionMask ? [getPolylinePoints(motionMask)] : [] ); const [zonePoints, setZonePoints] = useState( Object.keys(zones).reduce((memo, zone) => ({ ...memo, [zone]: getPolylinePoints(zones[zone].coordinates) }), {}) ); const [objectMaskPoints, setObjectMaskPoints] = useState( Object.keys(objectFilters).reduce( (memo, name) => ({ ...memo, [name]: Array.isArray(objectFilters[name].mask) ? objectFilters[name].mask.map((mask) => getPolylinePoints(mask)) : objectFilters[name].mask ? [getPolylinePoints(objectFilters[name].mask)] : [], }), {} ) ); const [editing, setEditing] = useState({ set: motionMaskPoints, key: 0, fn: setMotionMaskPoints }); const [success, setSuccess] = useState(); const [error, setError] = useState(); const handleUpdateEditable = useCallback( (newPoints) => { let newSet; if (Array.isArray(editing.set)) { newSet = [...editing.set]; newSet[editing.key] = newPoints; } else if (editing.subkey !== undefined) { newSet = { ...editing.set }; newSet[editing.key][editing.subkey] = newPoints; } else { newSet = { ...editing.set, [editing.key]: newPoints }; } editing.set = newSet; editing.fn(newSet); }, [editing] ); // Motion mask methods const handleAddMask = useCallback(() => { const newMotionMaskPoints = [...motionMaskPoints, []]; setMotionMaskPoints(newMotionMaskPoints); setEditing({ set: newMotionMaskPoints, key: newMotionMaskPoints.length - 1, fn: setMotionMaskPoints }); }, [motionMaskPoints, setMotionMaskPoints]); const handleEditMask = useCallback( (key) => { setEditing({ set: motionMaskPoints, key, fn: setMotionMaskPoints }); }, [setEditing, motionMaskPoints, setMotionMaskPoints] ); const handleRemoveMask = useCallback( (key) => { const newMotionMaskPoints = [...motionMaskPoints]; newMotionMaskPoints.splice(key, 1); setMotionMaskPoints(newMotionMaskPoints); }, [motionMaskPoints, setMotionMaskPoints] ); const handleCopyMotionMasks = useCallback(() => { const textToCopy = ` motion: mask: ${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]); 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) { setSuccess(response.data); } } catch (error) { if (error.response) { setError(error.response.data.message); } else { setError(error.message); } } }, [camera, motionMaskPoints]); // Zone methods const handleEditZone = useCallback( (key) => { setEditing({ set: zonePoints, key, fn: setZonePoints }); }, [setEditing, zonePoints, setZonePoints] ); const handleAddZone = useCallback(() => { const n = Object.keys(zonePoints).filter((name) => name.startsWith('zone_')).length; const zoneName = `zone_${n}`; const newZonePoints = { ...zonePoints, [zoneName]: [] }; setZonePoints(newZonePoints); setEditing({ set: newZonePoints, key: zoneName, fn: setZonePoints }); }, [zonePoints, setZonePoints]); const handleRemoveZone = useCallback( (key) => { const newZonePoints = { ...zonePoints }; delete newZonePoints[key]; setZonePoints(newZonePoints); }, [zonePoints, setZonePoints] ); const handleCopyZones = useCallback(async () => { const textToCopy = ` zones: ${Object.keys(zonePoints) .map( (zoneName) => ` ${zoneName}: coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`).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]); 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) { setSuccess(response.data); } } catch (error) { if (error.response) { setError(error.response.data.message); } else { setError(error.message); } } }, [camera, zonePoints]); // Object methods const handleEditObjectMask = useCallback( (key, subkey) => { setEditing({ set: objectMaskPoints, key, subkey, fn: setObjectMaskPoints }); }, [setEditing, objectMaskPoints, setObjectMaskPoints] ); const handleAddObjectMask = useCallback(() => { const n = Object.keys(objectMaskPoints).filter((name) => name.startsWith('object_')).length; const newObjectName = `object_${n}`; const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [[]] }; setObjectMaskPoints(newObjectMaskPoints); setEditing({ set: newObjectMaskPoints, key: newObjectName, subkey: 0, fn: setObjectMaskPoints }); }, [objectMaskPoints, setObjectMaskPoints, setEditing]); const handleRemoveObjectMask = useCallback( (key, subkey) => { const newObjectMaskPoints = { ...objectMaskPoints }; delete newObjectMaskPoints[key][subkey]; setObjectMaskPoints(newObjectMaskPoints); }, [objectMaskPoints, setObjectMaskPoints] ); const handleCopyObjectMasks = useCallback(async () => { await window.navigator.clipboard.writeText(` objects: filters: ${Object.keys(objectMaskPoints) .map((objectName) => objectMaskPoints[objectName].length ? ` ${objectName}: mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}` : '' ) .filter(Boolean) .join('\n')}`); }, [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) { setSuccess(response.data); } } catch (error) { if (error.response) { setError(error.response.data.message); } else { setError(error.message); } } }, [camera, objectMaskPoints]); const handleAddToObjectMask = useCallback( (key) => { const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] }; setObjectMaskPoints(newObjectMaskPoints); setEditing({ set: newObjectMaskPoints, key, subkey: newObjectMaskPoints[key].length - 1, fn: setObjectMaskPoints, }); }, [objectMaskPoints, setObjectMaskPoints, setEditing] ); const handleChangeSnap = useCallback( (id, value) => { setSnap(value); }, [setSnap] ); return (
This tool can help you create masks & zones for your {camera} camera.
config.yml file
restart your Frigate instance to save your changes.
}
header="Warning"
/>
{success &&
{yamlPrefix}
{Object.keys(points).map((mainkey) => {
if (isMulti) {
return (
{` ${mainkey}:\n mask:\n`}
{onAdd && showButtons ? (
) : null}
{points[mainkey].map((item, subkey) => (
))}
);
}
return (
);
})}