forked from Github/frigate
Compare commits
15 Commits
v0.8.0-rc1
...
v0.8.0-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b4e510b95 | ||
|
|
bb4f79cdfe | ||
|
|
e32e69c2d0 | ||
|
|
a71ae053e4 | ||
|
|
fcc9cd56cc | ||
|
|
b981a3110b | ||
|
|
2da50cc538 | ||
|
|
cb4a0aa594 | ||
|
|
52da1fddc7 | ||
|
|
14645ce4f8 | ||
|
|
97ce7f3028 | ||
|
|
3b5302f6ea | ||
|
|
74eb16f213 | ||
|
|
a3d6bf214c | ||
|
|
16121ffd00 |
16
Makefile
16
Makefile
@@ -9,49 +9,49 @@ web:
|
|||||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||||
|
|
||||||
amd64_wheels:
|
amd64_wheels:
|
||||||
docker build --tag blakeblackshear/frigate-wheels:1.0.0-amd64 --file docker/Dockerfile.wheels .
|
docker build --tag blakeblackshear/frigate-wheels:1.0.1-amd64 --file docker/Dockerfile.wheels .
|
||||||
|
|
||||||
amd64_ffmpeg:
|
amd64_ffmpeg:
|
||||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
|
docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
|
||||||
|
|
||||||
amd64_frigate: version web
|
amd64_frigate: version web
|
||||||
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.0 --file docker/Dockerfile.base .
|
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||||
docker build --tag frigate --file docker/Dockerfile.amd64 .
|
docker build --tag frigate --file docker/Dockerfile.amd64 .
|
||||||
|
|
||||||
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
|
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
|
||||||
|
|
||||||
amd64nvidia_wheels:
|
amd64nvidia_wheels:
|
||||||
docker build --tag blakeblackshear/frigate-wheels:1.0.0-amd64nvidia --file docker/Dockerfile.wheels .
|
docker build --tag blakeblackshear/frigate-wheels:1.0.1-amd64nvidia --file docker/Dockerfile.wheels .
|
||||||
|
|
||||||
amd64nvidia_ffmpeg:
|
amd64nvidia_ffmpeg:
|
||||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
|
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
|
||||||
|
|
||||||
amd64nvidia_frigate: version web
|
amd64nvidia_frigate: version web
|
||||||
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.0 --file docker/Dockerfile.base .
|
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||||
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
|
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
|
||||||
|
|
||||||
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
|
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
|
||||||
|
|
||||||
aarch64_wheels:
|
aarch64_wheels:
|
||||||
docker build --tag blakeblackshear/frigate-wheels:1.0.0-aarch64 --file docker/Dockerfile.wheels .
|
docker build --tag blakeblackshear/frigate-wheels:1.0.1-aarch64 --file docker/Dockerfile.wheels .
|
||||||
|
|
||||||
aarch64_ffmpeg:
|
aarch64_ffmpeg:
|
||||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
|
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
|
||||||
|
|
||||||
aarch64_frigate: version web
|
aarch64_frigate: version web
|
||||||
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.0 --file docker/Dockerfile.base .
|
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||||
docker build --tag frigate --file docker/Dockerfile.aarch64 .
|
docker build --tag frigate --file docker/Dockerfile.aarch64 .
|
||||||
|
|
||||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||||
|
|
||||||
armv7_wheels:
|
armv7_wheels:
|
||||||
docker build --tag blakeblackshear/frigate-wheels:1.0.0-armv7 --file docker/Dockerfile.wheels .
|
docker build --tag blakeblackshear/frigate-wheels:1.0.1-armv7 --file docker/Dockerfile.wheels .
|
||||||
|
|
||||||
armv7_ffmpeg:
|
armv7_ffmpeg:
|
||||||
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
|
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
|
||||||
|
|
||||||
armv7_frigate: version web
|
armv7_frigate: version web
|
||||||
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.0 --file docker/Dockerfile.base .
|
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
|
||||||
docker build --tag frigate --file docker/Dockerfile.armv7 .
|
docker build --tag frigate --file docker/Dockerfile.armv7 .
|
||||||
|
|
||||||
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ RUN pip3 install scikit-build
|
|||||||
|
|
||||||
RUN pip3 wheel --wheel-dir=/wheels \
|
RUN pip3 wheel --wheel-dir=/wheels \
|
||||||
opencv-python-headless \
|
opencv-python-headless \
|
||||||
numpy \
|
# pinning due to issue in 1.19.5 https://github.com/numpy/numpy/issues/18131
|
||||||
|
numpy==1.19.4 \
|
||||||
imutils \
|
imutils \
|
||||||
scipy \
|
scipy \
|
||||||
psutil \
|
psutil \
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ automation:
|
|||||||
data_template:
|
data_template:
|
||||||
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
|
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
|
||||||
data:
|
data:
|
||||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}.jpg"
|
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||||
tag: "{{trigger.payload_json['after']['id']}}"
|
tag: "{{trigger.payload_json['after']['id']}}"
|
||||||
|
|
||||||
- alias: When a person leaves a zone named yard
|
- alias: When a person leaves a zone named yard
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class FrigateApp():
|
|||||||
'camera_fps': mp.Value('d', 0.0),
|
'camera_fps': mp.Value('d', 0.0),
|
||||||
'skipped_fps': mp.Value('d', 0.0),
|
'skipped_fps': mp.Value('d', 0.0),
|
||||||
'process_fps': mp.Value('d', 0.0),
|
'process_fps': mp.Value('d', 0.0),
|
||||||
'detection_enabled': mp.Value('i', 1),
|
'detection_enabled': mp.Value('i', self.config.cameras[camera_name].detect.enabled),
|
||||||
'detection_fps': mp.Value('d', 0.0),
|
'detection_fps': mp.Value('d', 0.0),
|
||||||
'detection_frame': mp.Value('d', 0.0),
|
'detection_frame': mp.Value('d', 0.0),
|
||||||
'read_start': mp.Value('d', 0.0),
|
'read_start': mp.Value('d', 0.0),
|
||||||
@@ -202,7 +202,7 @@ class FrigateApp():
|
|||||||
try:
|
try:
|
||||||
self.init_config()
|
self.init_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error parsing config: {e}")
|
print(f"Error parsing config: {e}")
|
||||||
self.log_process.terminate()
|
self.log_process.terminate()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
self.set_environment_vars()
|
self.set_environment_vars()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
logger.debug(f"on_clips_toggle: {message.topic} {payload}")
|
logger.debug(f"on_clips_toggle: {message.topic} {payload}")
|
||||||
|
|
||||||
camera_name = message.topic.split('/')[-3]
|
camera_name = message.topic.split('/')[-3]
|
||||||
command = message.topic.split('/')[-1]
|
|
||||||
|
|
||||||
clips_settings = config.cameras[camera_name].clips
|
clips_settings = config.cameras[camera_name].clips
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
else:
|
else:
|
||||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||||
|
|
||||||
if command == "set":
|
|
||||||
state_topic = f"{message.topic[:-4]}/state"
|
state_topic = f"{message.topic[:-4]}/state"
|
||||||
client.publish(state_topic, payload, retain=True)
|
client.publish(state_topic, payload, retain=True)
|
||||||
|
|
||||||
@@ -39,7 +37,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
logger.debug(f"on_snapshots_toggle: {message.topic} {payload}")
|
logger.debug(f"on_snapshots_toggle: {message.topic} {payload}")
|
||||||
|
|
||||||
camera_name = message.topic.split('/')[-3]
|
camera_name = message.topic.split('/')[-3]
|
||||||
command = message.topic.split('/')[-1]
|
|
||||||
|
|
||||||
snapshots_settings = config.cameras[camera_name].snapshots
|
snapshots_settings = config.cameras[camera_name].snapshots
|
||||||
|
|
||||||
@@ -54,7 +51,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
else:
|
else:
|
||||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||||
|
|
||||||
if command == "set":
|
|
||||||
state_topic = f"{message.topic[:-4]}/state"
|
state_topic = f"{message.topic[:-4]}/state"
|
||||||
client.publish(state_topic, payload, retain=True)
|
client.publish(state_topic, payload, retain=True)
|
||||||
|
|
||||||
@@ -63,7 +59,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
logger.debug(f"on_detect_toggle: {message.topic} {payload}")
|
logger.debug(f"on_detect_toggle: {message.topic} {payload}")
|
||||||
|
|
||||||
camera_name = message.topic.split('/')[-3]
|
camera_name = message.topic.split('/')[-3]
|
||||||
command = message.topic.split('/')[-1]
|
|
||||||
|
|
||||||
detect_settings = config.cameras[camera_name].detect
|
detect_settings = config.cameras[camera_name].detect
|
||||||
|
|
||||||
@@ -80,7 +75,6 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
else:
|
else:
|
||||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||||
|
|
||||||
if command == "set":
|
|
||||||
state_topic = f"{message.topic[:-4]}/state"
|
state_topic = f"{message.topic[:-4]}/state"
|
||||||
client.publish(state_topic, payload, retain=True)
|
client.publish(state_topic, payload, retain=True)
|
||||||
|
|
||||||
@@ -105,9 +99,9 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
|
|
||||||
# register callbacks
|
# register callbacks
|
||||||
for name in config.cameras.keys():
|
for name in config.cameras.keys():
|
||||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/clips/#", on_clips_command)
|
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/clips/set", on_clips_command)
|
||||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/snapshots/#", on_snapshots_command)
|
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/snapshots/set", on_snapshots_command)
|
||||||
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/detect/#", on_detect_command)
|
client.message_callback_add(f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command)
|
||||||
|
|
||||||
if not mqtt_config.user is None:
|
if not mqtt_config.user is None:
|
||||||
client.username_pw_set(mqtt_config.user, password=mqtt_config.password)
|
client.username_pw_set(mqtt_config.user, password=mqtt_config.password)
|
||||||
@@ -121,11 +115,11 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
|||||||
|
|
||||||
for name in config.cameras.keys():
|
for name in config.cameras.keys():
|
||||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/clips/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
|
client.publish(f"{mqtt_config.topic_prefix}/{name}/clips/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
|
||||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/snapshots/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
|
client.publish(f"{mqtt_config.topic_prefix}/{name}/snapshots/state", 'ON' if config.cameras[name].snapshots.enabled else 'OFF', retain=True)
|
||||||
client.publish(f"{mqtt_config.topic_prefix}/{name}/detect/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
|
client.publish(f"{mqtt_config.topic_prefix}/{name}/detect/state", 'ON' if config.cameras[name].detect.enabled else 'OFF', retain=True)
|
||||||
|
|
||||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/clips/#")
|
client.subscribe(f"{mqtt_config.topic_prefix}/+/clips/set")
|
||||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/snapshots/#")
|
client.subscribe(f"{mqtt_config.topic_prefix}/+/snapshots/set")
|
||||||
client.subscribe(f"{mqtt_config.topic_prefix}/+/detect/#")
|
client.subscribe(f"{mqtt_config.topic_prefix}/+/detect/set")
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|||||||
@@ -183,7 +183,11 @@ class TrackedObject():
|
|||||||
if self.thumbnail_data is None:
|
if self.thumbnail_data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
|
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
|
||||||
|
except KeyError:
|
||||||
|
logger.warning(f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache")
|
||||||
|
return None
|
||||||
|
|
||||||
if bounding_box:
|
if bounding_box:
|
||||||
thickness = 2
|
thickness = 2
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
sub_filter 'href="/' 'href="$http_x_ingress_path/';
|
||||||
|
sub_filter 'url(/' 'url($http_x_ingress_path/';
|
||||||
|
sub_filter '"/js/' '"$http_x_ingress_path/js/';
|
||||||
|
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
|
||||||
|
sub_filter_types text/css application/javascript;
|
||||||
|
sub_filter_once off;
|
||||||
root /opt/frigate/web;
|
root /opt/frigate/web;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
|
||||||
packageOptions: {
|
packageOptions: {
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ export default function App() {
|
|||||||
<div />
|
<div />
|
||||||
) : (
|
) : (
|
||||||
<Config.Provider value={config}>
|
<Config.Provider value={config}>
|
||||||
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 ">
|
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="p-4">
|
<div className="p-4 min-w-0">
|
||||||
<Router>
|
<Router>
|
||||||
<CameraMap path="/cameras/:camera/editor" />
|
<CameraMap path="/cameras/:camera/editor" />
|
||||||
<Camera path="/cameras/:camera" />
|
<Camera path="/cameras/:camera" />
|
||||||
<Event path="/events/:eventId" />
|
<Event path="/events/:eventId" />
|
||||||
<Events path="/events" />
|
<Events path="/events" />
|
||||||
<Debug path="/debug" />
|
<Debug path="/debug" />
|
||||||
<Cameras path="/" />
|
<Cameras default path="/" />
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import AutoUpdatingCameraImage from './components/AutoUpdatingCameraImage';
|
||||||
|
import Box from './components/Box';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
@@ -32,45 +34,34 @@ export default function Camera({ camera, url }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<Heading size="2xl">{camera}</Heading>
|
<Heading size="2xl">{camera}</Heading>
|
||||||
<img
|
<Box>
|
||||||
width={cameraConfig.width}
|
<AutoUpdatingCameraImage camera={camera} searchParams={searchParamsString} />
|
||||||
height={cameraConfig.height}
|
</Box>
|
||||||
key={searchParamsString}
|
|
||||||
src={`${apiHost}/api/${camera}?${searchParamsString}`}
|
<Box className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
||||||
/>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
|
|
||||||
<Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} />
|
<Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} />
|
||||||
<Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} />
|
<Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} />
|
||||||
<Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} />
|
<Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} />
|
||||||
<Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} />
|
<Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} />
|
||||||
<Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
|
<Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
|
||||||
<Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
|
<Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
|
||||||
</div>
|
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
||||||
<div>
|
</Box>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
<Heading size="sm">Tracked objects</Heading>
|
<Heading size="sm">Tracked objects</Heading>
|
||||||
<ul className="flex flex-row flex-wrap space-x-4">
|
<div className="grid grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
{cameraConfig.objects.track.map((objectType) => {
|
{cameraConfig.objects.track.map((objectType) => {
|
||||||
return (
|
return (
|
||||||
<li key={objectType}>
|
<Box key={objectType} hover href={`/events?camera=${camera}&label=${objectType}`}>
|
||||||
<Link href={`/events?camera=${camera}&label=${objectType}`}>
|
<Heading size="sm">{objectType}</Heading>
|
||||||
<span className="capitalize">{objectType}</span>
|
|
||||||
<img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
|
<img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
|
||||||
</Link>
|
</Box>
|
||||||
</li>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<Heading size="sm">Options</Heading>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import Box from './components/Box';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
@@ -11,6 +12,7 @@ export default function CameraMasks({ camera, url }) {
|
|||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useContext(ApiHost);
|
||||||
const imageRef = useRef(null);
|
const imageRef = useRef(null);
|
||||||
const [imageScale, setImageScale] = useState(1);
|
const [imageScale, setImageScale] = useState(1);
|
||||||
|
const [snap, setSnap] = useState(true);
|
||||||
|
|
||||||
if (!(camera in config.cameras)) {
|
if (!(camera in config.cameras)) {
|
||||||
return <div>{`No camera named ${camera}`}</div>;
|
return <div>{`No camera named ${camera}`}</div>;
|
||||||
@@ -203,23 +205,39 @@ ${Object.keys(objectMaskPoints)
|
|||||||
.join('\n')}`);
|
.join('\n')}`);
|
||||||
}, [objectMaskPoints]);
|
}, [objectMaskPoints]);
|
||||||
|
|
||||||
|
const handleChangeSnap = useCallback(
|
||||||
|
(id, value) => {
|
||||||
|
setSnap(value);
|
||||||
|
},
|
||||||
|
[setSnap]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-col space-y-4" style={`max-width: ${width}px`}>
|
<div class="flex-col space-y-4">
|
||||||
<Heading size="2xl">{camera} mask & zone creator</Heading>
|
<Heading size="2xl">{camera} mask & zone creator</Heading>
|
||||||
|
|
||||||
|
<Box>
|
||||||
<p>
|
<p>
|
||||||
This tool can help you create masks & zones for your {camera} camera. When done, copy each mask configuration
|
This tool can help you create masks & zones for your {camera} camera. When done, copy each mask configuration
|
||||||
into your <code className="font-mono">config.yml</code> file restart your Frigate instance to save your changes.
|
into your <code className="font-mono">config.yml</code> file restart your Frigate instance to save your
|
||||||
|
changes.
|
||||||
</p>
|
</p>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img ref={imageRef} width={width} height={height} src={`${apiHost}/api/${camera}/latest.jpg`} />
|
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||||
<EditableMask
|
<EditableMask
|
||||||
onChange={handleUpdateEditable}
|
onChange={handleUpdateEditable}
|
||||||
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
||||||
scale={imageScale}
|
scale={imageScale}
|
||||||
|
snap={snap}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Switch checked={snap} label="Snap to edges" onChange={handleChangeSnap} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
<div class="flex-col space-y-4">
|
<div class="flex-col space-y-4">
|
||||||
<MaskValues
|
<MaskValues
|
||||||
@@ -276,14 +294,25 @@ function objectYamlKeyPrefix(points, key, subkey) {
|
|||||||
return ` - `;
|
return ` - `;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditableMask({ onChange, points, scale, width, height }) {
|
const MaskInset = 20;
|
||||||
|
|
||||||
|
function EditableMask({ onChange, points, scale, snap, width, height }) {
|
||||||
if (!points) {
|
if (!points) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const boundingRef = useRef(null);
|
const boundingRef = useRef(null);
|
||||||
|
|
||||||
function boundedSize(value, maxValue) {
|
function boundedSize(value, maxValue) {
|
||||||
return Math.min(Math.max(0, Math.round(value)), maxValue);
|
const newValue = Math.min(Math.max(0, Math.round(value)), maxValue);
|
||||||
|
if (snap) {
|
||||||
|
if (newValue <= MaskInset) {
|
||||||
|
return 0;
|
||||||
|
} else if (maxValue - newValue <= MaskInset) {
|
||||||
|
return maxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMovePoint = useCallback(
|
const handleMovePoint = useCallback(
|
||||||
@@ -291,35 +320,40 @@ function EditableMask({ onChange, points, scale, width, height }) {
|
|||||||
if (newX < 0 && newY < 0) {
|
if (newX < 0 && newY < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const x = boundedSize(newX / scale, width);
|
let x = boundedSize(newX / scale, width, snap);
|
||||||
const y = boundedSize(newY / scale, height);
|
let y = boundedSize(newY / scale, height, snap);
|
||||||
|
|
||||||
const newPoints = [...points];
|
const newPoints = [...points];
|
||||||
newPoints[index] = [x, y];
|
newPoints[index] = [x, y];
|
||||||
onChange(newPoints);
|
onChange(newPoints);
|
||||||
},
|
},
|
||||||
[scale, points]
|
[scale, points, snap]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add a new point between the closest two other points
|
// Add a new point between the closest two other points
|
||||||
const handleAddPoint = useCallback(
|
const handleAddPoint = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
const { offsetX, offsetY } = event;
|
const { offsetX, offsetY } = event;
|
||||||
const scaledX = boundedSize(offsetX / scale, width);
|
const scaledX = boundedSize((offsetX - MaskInset) / scale, width, snap);
|
||||||
const scaledY = boundedSize(offsetY / scale, height);
|
const scaledY = boundedSize((offsetY - MaskInset) / scale, height, snap);
|
||||||
const newPoint = [scaledX, scaledY];
|
const newPoint = [scaledX, scaledY];
|
||||||
const closest = points.reduce((a, b, i) => {
|
|
||||||
if (!a) {
|
let closest;
|
||||||
return b;
|
const { index } = points.reduce(
|
||||||
}
|
(result, point, i) => {
|
||||||
return distance(a, newPoint) < distance(b, newPoint) ? a : b;
|
const nextPoint = points.length === i + 1 ? points[0] : points[i + 1];
|
||||||
}, null);
|
const distance0 = Math.sqrt(Math.pow(point[0] - newPoint[0], 2) + Math.pow(point[1] - newPoint[1], 2));
|
||||||
const index = points.indexOf(closest);
|
const distance1 = Math.sqrt(Math.pow(point[0] - nextPoint[0], 2) + Math.pow(point[1] - nextPoint[1], 2));
|
||||||
|
const distance = distance0 + distance1;
|
||||||
|
return distance < result.distance ? { distance, index: i } : result;
|
||||||
|
},
|
||||||
|
{ distance: Infinity, index: -1 }
|
||||||
|
);
|
||||||
const newPoints = [...points];
|
const newPoints = [...points];
|
||||||
newPoints.splice(index, 0, newPoint);
|
newPoints.splice(index, 0, newPoint);
|
||||||
console.log(points, newPoints);
|
|
||||||
onChange(newPoints);
|
onChange(newPoints);
|
||||||
},
|
},
|
||||||
[scale, points, onChange]
|
[scale, points, onChange, snap]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemovePoint = useCallback(
|
const handleRemovePoint = useCallback(
|
||||||
@@ -334,7 +368,7 @@ function EditableMask({ onChange, points, scale, width, height }) {
|
|||||||
const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]);
|
const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onclick={handleAddPoint}>
|
<div className="absolute" style={`inset: -${MaskInset}px`}>
|
||||||
{!scaledPoints
|
{!scaledPoints
|
||||||
? null
|
? null
|
||||||
: scaledPoints.map(([x, y], i) => (
|
: scaledPoints.map(([x, y], i) => (
|
||||||
@@ -343,17 +377,12 @@ function EditableMask({ onChange, points, scale, width, height }) {
|
|||||||
index={i}
|
index={i}
|
||||||
onMove={handleMovePoint}
|
onMove={handleMovePoint}
|
||||||
onRemove={handleRemovePoint}
|
onRemove={handleRemovePoint}
|
||||||
x={x}
|
x={x + MaskInset}
|
||||||
y={y}
|
y={y + MaskInset}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<svg
|
<div className="absolute inset-0 right-0 bottom-0" onclick={handleAddPoint} ref={boundingRef} />
|
||||||
ref={boundingRef}
|
<svg width="100%" height="100%" className="absolute pointer-events-none" style={`inset: ${MaskInset}px`}>
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
className="absolute"
|
|
||||||
style="top: 0; left: 0; right: 0; bottom: 0;"
|
|
||||||
>
|
|
||||||
{!scaledPoints ? null : (
|
{!scaledPoints ? null : (
|
||||||
<g>
|
<g>
|
||||||
<polyline points={polylinePointsToPolyline(scaledPoints)} fill="rgba(244,0,0,0.5)" />
|
<polyline points={polylinePointsToPolyline(scaledPoints)} fill="rgba(244,0,0,0.5)" />
|
||||||
@@ -410,11 +439,7 @@ function MaskValues({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
|
||||||
className="overflow-hidden rounded border-gray-500 border-solid border p-2"
|
|
||||||
onmouseover={handleMousein}
|
|
||||||
onmouseout={handleMouseout}
|
|
||||||
>
|
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<Heading className="flex-grow self-center" size="base">
|
<Heading className="flex-grow self-center" size="base">
|
||||||
{title}
|
{title}
|
||||||
@@ -422,7 +447,7 @@ function MaskValues({
|
|||||||
<Button onClick={onCopy}>Copy</Button>
|
<Button onClick={onCopy}>Copy</Button>
|
||||||
<Button onClick={onCreate}>Add</Button>
|
<Button onClick={onCreate}>Add</Button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="overflow-hidden font-mono text-gray-900 dark:text-gray-100">
|
<pre class="relative overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2">
|
||||||
{yamlPrefix}
|
{yamlPrefix}
|
||||||
{Object.keys(points).map((mainkey) => {
|
{Object.keys(points).map((mainkey) => {
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
@@ -458,7 +483,7 @@ function MaskValues({
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,10 +514,6 @@ function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handl
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function distance([x0, y0], [x1, y1]) {
|
|
||||||
return Math.sqrt(Math.pow(x0 - x1, 2) + Math.pow(y0 - y1, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPolylinePoints(polyline) {
|
function getPolylinePoints(polyline) {
|
||||||
if (!polyline) {
|
if (!polyline) {
|
||||||
return;
|
return;
|
||||||
@@ -529,10 +550,13 @@ function PolyPoint({ boundingRef, index, x, y, onMove, onRemove }) {
|
|||||||
|
|
||||||
const handleDragOver = useCallback(
|
const handleDragOver = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.target !== boundingRef.current && !boundingRef.current.contains(event.target)) {
|
if (
|
||||||
|
!boundingRef.current ||
|
||||||
|
(event.target !== boundingRef.current && !boundingRef.current.contains(event.target))
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onMove(index, event.layerX, event.layerY - PolyPointRadius);
|
onMove(index, event.layerX - PolyPointRadius * 2, event.layerY - PolyPointRadius * 2);
|
||||||
},
|
},
|
||||||
[onMove, index, boundingRef.current]
|
[onMove, index, boundingRef.current]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import Box from './components/Box';
|
||||||
import Events from './Events';
|
import Events from './Events';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
@@ -26,11 +27,12 @@ function Camera({ name }) {
|
|||||||
const href = `/cameras/${name}`;
|
const href = `/cameras/${name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900">
|
<Box
|
||||||
<a className="dark:hover:text-gray-900" href={href}>
|
className="bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900"
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
<Heading size="base">{name}</Heading>
|
<Heading size="base">{name}</Heading>
|
||||||
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
||||||
</a>
|
</Box>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,97 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ApiHost } from './context';
|
import Heading from './components/Heading';
|
||||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
import Link from './components/Link';
|
||||||
|
import { ApiHost, Config } from './context';
|
||||||
|
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
|
||||||
|
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Debug() {
|
export default function Debug() {
|
||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useContext(ApiHost);
|
||||||
const [config, setConfig] = useState({});
|
const config = useContext(Config);
|
||||||
|
const [stats, setStats] = useState({});
|
||||||
|
const [timeoutId, setTimeoutId] = useState(null);
|
||||||
|
|
||||||
useEffect(async () => {
|
const fetchStats = useCallback(async () => {
|
||||||
const response = await fetch(`${apiHost}/api/stats`);
|
const statsResponse = await fetch(`${apiHost}/api/stats`);
|
||||||
const data = response.ok ? await response.json() : {};
|
const stats = statsResponse.ok ? await statsResponse.json() : {};
|
||||||
setConfig(data);
|
setStats(stats);
|
||||||
|
setTimeoutId(setTimeout(fetchStats, 1000));
|
||||||
|
}, [setStats]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <pre>{JSON.stringify(config, null, 2)}</pre>;
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [timeoutId]);
|
||||||
|
|
||||||
|
const { detectors, detection_fps, service, ...cameras } = stats;
|
||||||
|
if (!service) {
|
||||||
|
return 'loading…';
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectorNames = Object.keys(detectors);
|
||||||
|
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
|
||||||
|
|
||||||
|
const cameraNames = Object.keys(cameras);
|
||||||
|
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading>
|
||||||
|
Debug <span className="text-sm">{service.version}</span>
|
||||||
|
</Heading>
|
||||||
|
<Table className="w-full">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>detector</Th>
|
||||||
|
{detectorDataKeys.map((name) => (
|
||||||
|
<Th>{name.replace('_', ' ')}</Th>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{detectorNames.map((detector, i) => (
|
||||||
|
<Tr index={i}>
|
||||||
|
<Td>{detector}</Td>
|
||||||
|
{detectorDataKeys.map((name) => (
|
||||||
|
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Table className="w-full">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>camera</Th>
|
||||||
|
{cameraDataKeys.map((name) => (
|
||||||
|
<Th>{name.replace('_', ' ')}</Th>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{cameraNames.map((camera, i) => (
|
||||||
|
<Tr index={i}>
|
||||||
|
<Td>
|
||||||
|
<Link href={`/cameras/${camera}`}>{camera}</Link>
|
||||||
|
</Td>
|
||||||
|
{cameraDataKeys.map((name) => (
|
||||||
|
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Heading size="sm">Config</Heading>
|
||||||
|
<pre className="font-mono overflow-y-scroll overflow-x-scroll max-h-96 rounded bg-white dark:bg-gray-900">
|
||||||
|
{JSON.stringify(config, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { h } from 'preact';
|
import { h, Fragment } from 'preact';
|
||||||
import { ApiHost } from './context';
|
import { ApiHost } from './context';
|
||||||
|
import Box from './components/Box';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
|
import Link from './components/Link';
|
||||||
|
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
|
||||||
import { useContext, useEffect, useState } from 'preact/hooks';
|
import { useContext, useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Event({ eventId }) {
|
export default function Event({ eventId }) {
|
||||||
@@ -22,24 +25,66 @@ export default function Event({ eventId }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const datetime = new Date(data.start_time * 1000);
|
const startime = new Date(data.start_time * 1000);
|
||||||
|
const endtime = new Date(data.end_time * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<Heading>
|
<Heading>
|
||||||
{data.camera} {data.label} <span className="text-sm">{datetime.toLocaleString()}</span>
|
{data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<img
|
|
||||||
src={`${apiHost}/clips/${data.camera}-${eventId}.jpg`}
|
<Box>
|
||||||
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
|
|
||||||
/>
|
|
||||||
{data.has_clip ? (
|
{data.has_clip ? (
|
||||||
<video className="w-96" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
|
<Fragment>
|
||||||
|
<Heading size="sm">Clip</Heading>
|
||||||
|
<video className="w-100" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
|
||||||
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
<p>No clip available</p>
|
<p>No clip available</p>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
<Box>
|
||||||
|
<Heading size="sm">{data.has_snapshot ? 'Best image' : 'Thumbnail'}</Heading>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
data.has_snapshot
|
||||||
|
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
|
||||||
|
: `data:image/jpeg;base64,${data.thumbnail}`
|
||||||
|
}
|
||||||
|
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Th>Key</Th>
|
||||||
|
<Th>Value</Th>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td>Camera</Td>
|
||||||
|
<Td>
|
||||||
|
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr index={1}>
|
||||||
|
<Td>Timeframe</Td>
|
||||||
|
<Td>
|
||||||
|
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Score</Td>
|
||||||
|
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr index={1}>
|
||||||
|
<Td>Zones</Td>
|
||||||
|
<Td>{data.zones.join(', ')}</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { ApiHost } from './context';
|
import { ApiHost } from './context';
|
||||||
|
import Box from './components/Box';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
@@ -19,11 +20,17 @@ export default function Events({ url } = {}) {
|
|||||||
setEvents(data);
|
setEvents(data);
|
||||||
}, [searchParamsString]);
|
}, [searchParamsString]);
|
||||||
|
|
||||||
|
const searchKeys = Array.from(searchParams.keys());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<Heading>Events</Heading>
|
<Heading>Events</Heading>
|
||||||
|
|
||||||
|
{searchKeys.length ? (
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm">Filters</Heading>
|
||||||
<div className="flex flex-wrap space-x-2">
|
<div className="flex flex-wrap space-x-2">
|
||||||
{Array.from(searchParams.keys()).map((filterKey) => (
|
{searchKeys.map((filterKey) => (
|
||||||
<UnFilterable
|
<UnFilterable
|
||||||
paramName={filterKey}
|
paramName={filterKey}
|
||||||
searchParams={searchParamsString}
|
searchParams={searchParamsString}
|
||||||
@@ -31,6 +38,10 @@ export default function Events({ url } = {}) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Box className="min-w-0 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -56,7 +67,7 @@ export default function Events({ url } = {}) {
|
|||||||
<Tr key={id} index={i}>
|
<Tr key={id} index={i}>
|
||||||
<Td>
|
<Td>
|
||||||
<a href={`/events/${id}`}>
|
<a href={`/events/${id}`}>
|
||||||
<img className="w-32" src={`data:image/jpeg;base64,${thumbnail}`} />
|
<img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
|
||||||
</a>
|
</a>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
@@ -84,6 +95,7 @@ export default function Events({ url } = {}) {
|
|||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
27
web/src/components/AutoUpdatingCameraImage.jsx
Normal file
27
web/src/components/AutoUpdatingCameraImage.jsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { ApiHost, Config } from '../context';
|
||||||
|
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
||||||
|
const config = useContext(Config);
|
||||||
|
const apiHost = useContext(ApiHost);
|
||||||
|
const cameraConfig = config.cameras[camera];
|
||||||
|
|
||||||
|
const [key, setKey] = useState(Date.now());
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setKey(Date.now());
|
||||||
|
}, 500);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [key, searchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className="w-full"
|
||||||
|
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
|
||||||
|
alt={`Auto-updating ${camera} image`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/src/components/Box.jsx
Normal file
16
web/src/components/Box.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
|
||||||
|
export default function Box({ children, className = '', hover = false, href, ...props }) {
|
||||||
|
const Element = href ? 'a' : 'div';
|
||||||
|
return (
|
||||||
|
<Element
|
||||||
|
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 ${
|
||||||
|
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
|
||||||
|
} ${className}`}
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Element>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,12 +2,18 @@ import { h } from 'preact';
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
|
const BUTTON_COLORS = {
|
||||||
|
blue: { normal: 'bg-blue-500', hover: 'hover:bg-blue-400' },
|
||||||
|
red: { normal: 'bg-red-500', hover: 'hover:bg-red-400' },
|
||||||
|
green: { normal: 'bg-green-500', hover: 'hover:bg-green-400' },
|
||||||
|
};
|
||||||
|
|
||||||
export default function Button({ children, className, color = 'blue', onClick, size, ...attrs }) {
|
export default function Button({ children, className, color = 'blue', onClick, size, ...attrs }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
className={`rounded bg-${color}-500 text-white pl-4 pr-4 pt-2 pb-2 font-bold shadow hover:bg-${color}-400 hover:shadow-lg cursor-pointer ${className}`}
|
className={`rounded ${BUTTON_COLORS[color].normal} text-white pl-4 pr-4 pt-2 pb-2 font-bold shadow ${BUTTON_COLORS[color].hover} hover:shadow-lg cursor-pointer ${className}`}
|
||||||
onClick={onClick || noop}
|
onClick={onClick || noop}
|
||||||
{...attrs}
|
{...attrs}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
export default function Heading({ children, className = '', size = '2xl' }) {
|
export default function Heading({ children, className = '', size = '2xl' }) {
|
||||||
return (
|
return <h1 className={`font-semibold tracking-widest uppercase text-${size} ${className}`}>{children}</h1>;
|
||||||
<h1 className={`font-semibold tracking-widest text-gray-900 uppercase dark:text-white text-${size} ${className}`}>
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||||||
|
|
||||||
export default function Link({ className, children, href, ...props }) {
|
export default function Link({ className, children, href, ...props }) {
|
||||||
return (
|
return (
|
||||||
<a className={`text-blue-500 hover:underline ${className}`} href={href} {...props}>
|
<a className={`text-blue-500 dark:text-blue-400 hover:underline ${className}`} href={href} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ export default function Switch({ checked, label, id, onChange }) {
|
|||||||
<label for={id} className="flex items-center cursor-pointer">
|
<label for={id} className="flex items-center cursor-pointer">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input id={id} type="checkbox" className="hidden" onChange={handleChange} checked={checked} />
|
<input id={id} type="checkbox" className="hidden" onChange={handleChange} checked={checked} />
|
||||||
<div className="toggle__line w-12 h-6 bg-gray-400 rounded-full shadow-inner" />
|
<div
|
||||||
|
className={`transition-colors toggle__line w-12 h-6 ${
|
||||||
|
!checked ? 'bg-gray-400' : 'bg-blue-400'
|
||||||
|
} rounded-full shadow-inner`}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="transition-transform absolute w-6 h-6 bg-white rounded-full shadow-md inset-y-0 left-0"
|
className="transition-transform absolute w-6 h-6 bg-white rounded-full shadow-md inset-y-0 left-0"
|
||||||
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
|
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
|
||||||
export function Table({ children }) {
|
export function Table({ children, className = '' }) {
|
||||||
return <table className="table-auto border-collapse text-gray-900 dark:text-gray-200">{children}</table>;
|
return (
|
||||||
|
<table className={`table-auto border-collapse text-gray-900 dark:text-gray-200 ${className}`}>{children}</table>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Thead({ children }) {
|
export function Thead({ children, className = '' }) {
|
||||||
return <thead className="">{children}</thead>;
|
return <thead className={`${className}`}>{children}</thead>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tbody({ children }) {
|
export function Tbody({ children, className = '' }) {
|
||||||
return <tbody className="">{children}</tbody>;
|
return <tbody className={`${className}`}>{children}</tbody>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tfoot({ children }) {
|
export function Tfoot({ children, className = '' }) {
|
||||||
return <tfoot className="">{children}</tfoot>;
|
return <tfoot className={`${className}`}>{children}</tfoot>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tr({ children, index }) {
|
export function Tr({ children, className = '', index }) {
|
||||||
return <tr className={`${index % 2 ? 'bg-gray-200 ' : ''}`}>{children}</tr>;
|
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-700' : ''} ${className}`}>{children}</tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Th({ children }) {
|
export function Th({ children, className = '' }) {
|
||||||
return <th className="border-b-2 border-gray-400 p-4 text-left">{children}</th>;
|
return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Td({ children }) {
|
export function Td({ children, className = '' }) {
|
||||||
return <td className="p-4">{children}</td>;
|
return <td className={`p-4 ${className}`}>{children}</td>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import { createContext } from 'preact';
|
|||||||
|
|
||||||
export const Config = createContext({});
|
export const Config = createContext({});
|
||||||
|
|
||||||
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || '');
|
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
|
||||||
|
|||||||
Reference in New Issue
Block a user