forked from Github/frigate
Compare commits
205 Commits
v0.8.0
...
v0.8.0-rc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
714d76887d | ||
|
|
0c0e1416ff | ||
|
|
c6044ba9a1 | ||
|
|
a7739a0a62 | ||
|
|
84ed126db6 | ||
|
|
a76f54c326 | ||
|
|
b93d354c60 | ||
|
|
14d218af46 | ||
|
|
bd4973e3f7 | ||
|
|
d94f81969b | ||
|
|
d32fed2c01 | ||
|
|
7b4e510b95 | ||
|
|
bb4f79cdfe | ||
|
|
e32e69c2d0 | ||
|
|
a71ae053e4 | ||
|
|
fcc9cd56cc | ||
|
|
b981a3110b | ||
|
|
2da50cc538 | ||
|
|
cb4a0aa594 | ||
|
|
52da1fddc7 | ||
|
|
14645ce4f8 | ||
|
|
97ce7f3028 | ||
|
|
3b5302f6ea | ||
|
|
74eb16f213 | ||
|
|
a3d6bf214c | ||
|
|
16121ffd00 | ||
|
|
91628bd5d8 | ||
|
|
b10b64bf57 | ||
|
|
749c34be9f | ||
|
|
8cfdfab985 | ||
|
|
ef25f8a31e | ||
|
|
2a0551a08a | ||
|
|
0b80419f15 | ||
|
|
0dc81117aa | ||
|
|
49b29d72a7 | ||
|
|
21ece238ff | ||
|
|
f6ba3f2daa | ||
|
|
bb0d3cb59a | ||
|
|
ca9b6d6c5c | ||
|
|
3103ad2bfe | ||
|
|
eab3998ad0 | ||
|
|
a3dfd3a8e0 | ||
|
|
f1c3087775 | ||
|
|
1be91ed3f2 | ||
|
|
fd83c4f229 | ||
|
|
de99221ad5 | ||
|
|
6892ce56ac | ||
|
|
41cea6f62e | ||
|
|
4bbffa97df | ||
|
|
614f8abfef | ||
|
|
14289b5fd1 | ||
|
|
4164beff1c | ||
|
|
9b3ab486de | ||
|
|
232a49814a | ||
|
|
6c61f0b135 | ||
|
|
c572cec253 | ||
|
|
d4941f2a5f | ||
|
|
bf5ec2f65f | ||
|
|
f8e21584b6 | ||
|
|
3cba83f84b | ||
|
|
dcb4255d7e | ||
|
|
9fc3c0dc2f | ||
|
|
a78830b48e | ||
|
|
949fbadcdc | ||
|
|
12c9e63b13 | ||
|
|
157b230702 | ||
|
|
c69299d659 | ||
|
|
285d630770 | ||
|
|
b9318092f4 | ||
|
|
905c361d52 | ||
|
|
4443abbc49 | ||
|
|
dabb36ad93 | ||
|
|
2bc8736fd9 | ||
|
|
e9b3b09cc2 | ||
|
|
ca337c32b4 | ||
|
|
24b8bd7c85 | ||
|
|
3ad75a441d | ||
|
|
f006e9be8d | ||
|
|
03f3ba8008 | ||
|
|
96a44eb7bf | ||
|
|
006782fe3d | ||
|
|
ff3e95bbf7 | ||
|
|
4b95a37e65 | ||
|
|
38c661b3a8 | ||
|
|
0d6e4f6a66 | ||
|
|
1ad2219f1c | ||
|
|
dfcdd289c3 | ||
|
|
32f5f2cca9 | ||
|
|
24bfe9f3e8 | ||
|
|
004667dc99 | ||
|
|
9d785dc781 | ||
|
|
cbba5a7af0 | ||
|
|
29b29ee349 | ||
|
|
9ad53e09af | ||
|
|
c9278991c9 | ||
|
|
729de48934 | ||
|
|
7476bff5fb | ||
|
|
1e9eae8d9a | ||
|
|
8113a53381 | ||
|
|
72833686f1 | ||
|
|
096c21f105 | ||
|
|
181f66357b | ||
|
|
a54fbc483c | ||
|
|
92d5a002d3 | ||
|
|
f9184903d7 | ||
|
|
91cde6ce7b | ||
|
|
186a4587c7 | ||
|
|
6049acb1f3 | ||
|
|
2d2ebf313c | ||
|
|
3d329dcb52 | ||
|
|
06854fc34f | ||
|
|
e01e14d866 | ||
|
|
3dfd251ebb | ||
|
|
dcea807f77 | ||
|
|
87d83ff33a | ||
|
|
1d31cbdf0d | ||
|
|
e05b27b8dc | ||
|
|
7111bd208e | ||
|
|
04a80280da | ||
|
|
3bda092140 | ||
|
|
9086820479 | ||
|
|
d1da57aedc | ||
|
|
6ded12c566 | ||
|
|
70352566a7 | ||
|
|
cf5cc86588 | ||
|
|
e41db49ab8 | ||
|
|
1b7effafee | ||
|
|
69e9e0b0bf | ||
|
|
89624df411 | ||
|
|
d1a7405211 | ||
|
|
040f8c7c20 | ||
|
|
6d7acabf4c | ||
|
|
45a8b42157 | ||
|
|
8785be24b7 | ||
|
|
cc0812540c | ||
|
|
5cf38ca4f7 | ||
|
|
7e4395c30e | ||
|
|
598d3aeda2 | ||
|
|
012dbf81f7 | ||
|
|
f869def12e | ||
|
|
31f7666337 | ||
|
|
9e339acbca | ||
|
|
8f8054a299 | ||
|
|
f7021eec4c | ||
|
|
c124153da4 | ||
|
|
706c2f921e | ||
|
|
de1d66bcb9 | ||
|
|
4502ca8e80 | ||
|
|
32a66fe5e8 | ||
|
|
e1251aafdb | ||
|
|
587494068c | ||
|
|
7a4d90a47a | ||
|
|
d06b587d33 | ||
|
|
eef70e434b | ||
|
|
b39da3ee01 | ||
|
|
e07c4e0d8c | ||
|
|
2f41ba6f77 | ||
|
|
bf95af0f22 | ||
|
|
2e15847f86 | ||
|
|
5992e85dc8 | ||
|
|
24d416b869 | ||
|
|
5dbf368c4b | ||
|
|
7d56fe105f | ||
|
|
e9327aa18c | ||
|
|
df56e079de | ||
|
|
8c5bfbd187 | ||
|
|
2613e74f97 | ||
|
|
9a7fb96357 | ||
|
|
37f9dfed92 | ||
|
|
68c1544808 | ||
|
|
2b3d3c5824 | ||
|
|
efea87a3ea | ||
|
|
977785fb10 | ||
|
|
4e113e62c0 | ||
|
|
5080b2d781 | ||
|
|
5cfd6d1edb | ||
|
|
27ae4d8ab0 | ||
|
|
3db33302ec | ||
|
|
f2910d48e0 | ||
|
|
cf0f8892e2 | ||
|
|
4d22e172ff | ||
|
|
8874a55b0f | ||
|
|
24b703a875 | ||
|
|
8b8f5b5c40 | ||
|
|
eac81136d2 | ||
|
|
d1e27b43ea | ||
|
|
105dcb7094 | ||
|
|
c0a16efdc1 | ||
|
|
2800c54743 | ||
|
|
2a24e8abcb | ||
|
|
37ee746ebb | ||
|
|
7ee6bfe855 | ||
|
|
40f57a8754 | ||
|
|
e0da462223 | ||
|
|
47a9fc4292 | ||
|
|
03fe5158db | ||
|
|
72be6b480d | ||
|
|
a8964dcc1f | ||
|
|
732e91ee42 | ||
|
|
27da080ce6 | ||
|
|
075d06b108 | ||
|
|
95dc17ffcd | ||
|
|
408b53f8b4 | ||
|
|
3ef68a297a | ||
|
|
3e9b3711dc |
16
README.md
16
README.md
@@ -14,25 +14,9 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
|
||||
- Uses a very low overhead motion detection to determine where to run object detection
|
||||
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
||||
- Communicates over MQTT for easy integration into other systems
|
||||
- Records video clips of detected objects
|
||||
- 24/7 recording
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
|
||||
## Documentation
|
||||
|
||||
View the documentation at https://blakeblackshear.github.io/frigate
|
||||
|
||||
## Screenshots
|
||||
Integration into HomeAssistant
|
||||
<div>
|
||||
<a href="docs/static/img/media_browser.png"><img src="docs/static/img/media_browser.png" height=400></a>
|
||||
<a href="docs/static/img/notification.png"><img src="docs/static/img/notification.png" height=400></a>
|
||||
</div>
|
||||
|
||||
Also comes with a builtin UI:
|
||||
<div>
|
||||
<a href="docs/static/img/home-ui.png"><img src="docs/static/img/home-ui.png" height=400></a>
|
||||
<a href="docs/static/img/camera-ui.png"><img src="docs/static/img/camera-ui.png" height=400></a>
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
@@ -21,7 +21,6 @@ HassOS users can install via the addon repository. Frigate requires an MQTT serv
|
||||
## Docker
|
||||
|
||||
Make sure you choose the right image for your architecture:
|
||||
|
||||
|Arch|Image Name|
|
||||
|-|-|
|
||||
|amd64|blakeblackshear/frigate:stable-amd64|
|
||||
|
||||
@@ -6,6 +6,7 @@ title: Troubleshooting
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
||||
|
||||
Example:
|
||||

|
||||
|
||||
## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
|
||||
|
||||
@@ -9,10 +9,6 @@ module.exports = {
|
||||
organizationName: 'blakeblackshear',
|
||||
projectName: 'frigate',
|
||||
themeConfig: {
|
||||
algolia: {
|
||||
apiKey: '81ec882db78f7fed05c51daf973f0362',
|
||||
indexName: 'frigate'
|
||||
},
|
||||
navbar: {
|
||||
title: 'Frigate',
|
||||
logo: {
|
||||
|
||||
BIN
docs/static/img/camera-ui.png
vendored
BIN
docs/static/img/camera-ui.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 944 KiB |
BIN
docs/static/img/events-ui.png
vendored
BIN
docs/static/img/events-ui.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/home-ui.png
vendored
BIN
docs/static/img/home-ui.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 MiB |
@@ -26,7 +26,7 @@ export default function App() {
|
||||
<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 text-gray-900 dark:text-white">
|
||||
<Sidebar />
|
||||
<div className="flex-auto p-4 lg:pl-8 lg:pr-8 min-w-0">
|
||||
<div className="p-4 min-w-0">
|
||||
<Router>
|
||||
<CameraMap path="/cameras/:camera/editor" />
|
||||
<Camera path="/cameras/:camera" />
|
||||
@@ -39,4 +39,5 @@ export default function App() {
|
||||
</div>
|
||||
</Config.Provider>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,26 +27,14 @@ export default function CameraMasks({ camera, url }) {
|
||||
zones,
|
||||
} = cameraConfig;
|
||||
|
||||
const resizeObserver = useMemo(
|
||||
() =>
|
||||
new ResizeObserver((entries) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (Array.isArray(entries) && entries.length) {
|
||||
const scaledWidth = entries[0].contentRect.width;
|
||||
const scale = scaledWidth / width;
|
||||
setImageScale(scale);
|
||||
}
|
||||
});
|
||||
}),
|
||||
[camera, width, setImageScale]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageRef.current) {
|
||||
return;
|
||||
}
|
||||
resizeObserver.observe(imageRef.current);
|
||||
}, [resizeObserver, imageRef.current]);
|
||||
const scaledWidth = imageRef.current.width;
|
||||
const scale = scaledWidth / width;
|
||||
setImageScale(scale);
|
||||
}, [imageRef.current, setImageScale]);
|
||||
|
||||
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
||||
Array.isArray(motionMask)
|
||||
@@ -189,7 +177,7 @@ ${Object.keys(zonePoints)
|
||||
const handleAddObjectMask = useCallback(() => {
|
||||
const n = Object.keys(objectMaskPoints).filter((name) => name.startsWith('object_')).length;
|
||||
const newObjectName = `object_${n}`;
|
||||
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [[]] };
|
||||
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [] };
|
||||
setObjectMaskPoints(newObjectMaskPoints);
|
||||
setEditing({ set: newObjectMaskPoints, key: newObjectName, subkey: 0, fn: setObjectMaskPoints });
|
||||
}, [objectMaskPoints, setObjectMaskPoints, setEditing]);
|
||||
@@ -197,7 +185,7 @@ ${Object.keys(zonePoints)
|
||||
const handleRemoveObjectMask = useCallback(
|
||||
(key, subkey) => {
|
||||
const newObjectMaskPoints = { ...objectMaskPoints };
|
||||
delete newObjectMaskPoints[key][subkey];
|
||||
delete newObjectMaskPoints[key];
|
||||
setObjectMaskPoints(newObjectMaskPoints);
|
||||
},
|
||||
[objectMaskPoints, setObjectMaskPoints]
|
||||
@@ -217,20 +205,6 @@ ${Object.keys(objectMaskPoints)
|
||||
.join('\n')}`);
|
||||
}, [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);
|
||||
@@ -252,10 +226,10 @@ ${Object.keys(objectMaskPoints)
|
||||
|
||||
<Box className="space-y-4">
|
||||
<div className="relative">
|
||||
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||
<EditableMask
|
||||
onChange={handleUpdateEditable}
|
||||
points={'subkey' in editing ? 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}
|
||||
snap={snap}
|
||||
width={width}
|
||||
@@ -294,7 +268,6 @@ ${Object.keys(objectMaskPoints)
|
||||
isMulti
|
||||
editing={editing}
|
||||
title="Object masks"
|
||||
onAdd={handleAddToObjectMask}
|
||||
onCopy={handleCopyObjectMasks}
|
||||
onCreate={handleAddObjectMask}
|
||||
onEdit={handleEditObjectMask}
|
||||
@@ -424,7 +397,6 @@ function MaskValues({
|
||||
isMulti = false,
|
||||
editing,
|
||||
title,
|
||||
onAdd,
|
||||
onCopy,
|
||||
onCreate,
|
||||
onEdit,
|
||||
@@ -466,14 +438,6 @@ function MaskValues({
|
||||
[onRemove]
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(
|
||||
(event) => {
|
||||
const { key } = event.target.dataset;
|
||||
onAdd(key);
|
||||
},
|
||||
[onAdd]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
|
||||
<div class="flex space-x-4">
|
||||
@@ -490,20 +454,15 @@ function MaskValues({
|
||||
return (
|
||||
<div>
|
||||
{` ${mainkey}:\n mask:\n`}
|
||||
{onAdd && showButtons ? (
|
||||
<Button className="absolute -mt-12 right-0 font-sans" data-key={mainkey} onClick={handleAdd}>
|
||||
{`Add to ${mainkey}`}
|
||||
</Button>
|
||||
) : null}
|
||||
{points[mainkey].map((item, subkey) => (
|
||||
<Item
|
||||
mainkey={mainkey}
|
||||
subkey={subkey}
|
||||
editing={editing}
|
||||
handleEdit={handleEdit}
|
||||
handleRemove={handleRemove}
|
||||
points={item}
|
||||
showButtons={showButtons}
|
||||
handleRemove={handleRemove}
|
||||
yamlKeyPrefix={yamlKeyPrefix}
|
||||
/>
|
||||
))}
|
||||
@@ -514,11 +473,10 @@ function MaskValues({
|
||||
<Item
|
||||
mainkey={mainkey}
|
||||
editing={editing}
|
||||
handleAdd={onAdd ? handleAdd : undefined}
|
||||
handleEdit={handleEdit}
|
||||
handleRemove={handleRemove}
|
||||
points={points[mainkey]}
|
||||
showButtons={showButtons}
|
||||
handleRemove={handleRemove}
|
||||
yamlKeyPrefix={yamlKeyPrefix}
|
||||
/>
|
||||
);
|
||||
@@ -529,7 +487,7 @@ function MaskValues({
|
||||
);
|
||||
}
|
||||
|
||||
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleAdd, handleRemove, yamlKeyPrefix }) {
|
||||
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleRemove, yamlKeyPrefix }) {
|
||||
return (
|
||||
<span
|
||||
data-key={mainkey}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import Box from './components/Box';
|
||||
import CameraImage from './components/CameraImage';
|
||||
import Events from './Events';
|
||||
import Heading from './components/Heading';
|
||||
import { route } from 'preact-router';
|
||||
@@ -24,6 +23,7 @@ export default function Cameras() {
|
||||
}
|
||||
|
||||
function Camera({ name }) {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const href = `/cameras/${name}`;
|
||||
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ function Camera({ name }) {
|
||||
href={href}
|
||||
>
|
||||
<Heading size="base">{name}</Heading>
|
||||
<CameraImage camera={name} />
|
||||
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { h } from 'preact';
|
||||
import Box from './components/Box';
|
||||
import Button from './components/Button';
|
||||
import Heading from './components/Heading';
|
||||
import Link from './components/Link';
|
||||
import { ApiHost, Config } from './context';
|
||||
@@ -41,73 +39,59 @@ export default function Debug() {
|
||||
const cameraNames = Object.keys(cameras);
|
||||
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
|
||||
|
||||
const handleCopyConfig = useCallback(async () => {
|
||||
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Heading>
|
||||
Debug <span className="text-sm">{service.version}</span>
|
||||
</Heading>
|
||||
|
||||
<Box>
|
||||
<Table className="w-full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>detector</Th>
|
||||
<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) => (
|
||||
<Th>{name.replace('_', ' ')}</Th>
|
||||
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
|
||||
))}
|
||||
</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>
|
||||
</Box>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
<Box>
|
||||
<Table className="w-full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>camera</Th>
|
||||
<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) => (
|
||||
<Th>{name.replace('_', ' ')}</Th>
|
||||
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
|
||||
))}
|
||||
</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>
|
||||
</Box>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
<Box className="relative">
|
||||
<Heading size="sm">Config</Heading>
|
||||
<Button className="absolute top-4 right-8" onClick={handleCopyConfig}>
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
|
||||
{JSON.stringify(config, null, 2)}
|
||||
</pre>
|
||||
</Box>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Events({ url } = {}) {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [events, setEvents] = useState([]);
|
||||
|
||||
const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`);
|
||||
const searchParams = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`).searchParams;
|
||||
const searchParamsString = searchParams.toString();
|
||||
|
||||
useEffect(async () => {
|
||||
@@ -23,7 +23,7 @@ export default function Events({ url } = {}) {
|
||||
const searchKeys = Array.from(searchParams.keys());
|
||||
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="space-y-4">
|
||||
<Heading>Events</Heading>
|
||||
|
||||
{searchKeys.length ? (
|
||||
@@ -32,10 +32,9 @@ export default function Events({ url } = {}) {
|
||||
<div className="flex flex-wrap space-x-2">
|
||||
{searchKeys.map((filterKey) => (
|
||||
<UnFilterable
|
||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
||||
paramName={filterKey}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -43,7 +42,7 @@ export default function Events({ url } = {}) {
|
||||
) : null}
|
||||
|
||||
<Box className="min-w-0 overflow-auto">
|
||||
<Table className="w-full">
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th></Th>
|
||||
@@ -72,32 +71,17 @@ export default function Events({ url } = {}) {
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="camera"
|
||||
name={camera}
|
||||
/>
|
||||
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="label"
|
||||
name={label}
|
||||
/>
|
||||
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
|
||||
</Td>
|
||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||
<Td>
|
||||
<ul>
|
||||
{zones.map((zone) => (
|
||||
<li>
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="zone"
|
||||
name={zone}
|
||||
/>
|
||||
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -116,19 +100,19 @@ export default function Events({ url } = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
function Filterable({ pathname, searchParams, paramName, name }) {
|
||||
function Filterable({ searchParams, paramName, name }) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set(paramName, name);
|
||||
return <Link href={`${pathname}?${params.toString()}`}>{name}</Link>;
|
||||
return <Link href={`?${params.toString()}`}>{name}</Link>;
|
||||
}
|
||||
|
||||
function UnFilterable({ pathname, searchParams, paramName, name }) {
|
||||
function UnFilterable({ searchParams, paramName, name }) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.delete(paramName);
|
||||
return (
|
||||
<a
|
||||
className="bg-gray-700 text-white px-3 py-1 rounded-md hover:bg-gray-300 hover:text-gray-900 dark:bg-gray-300 dark:text-gray-900 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
href={`${pathname}?${params.toString()}`}
|
||||
href={`?${params.toString()}`}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import { h } from 'preact';
|
||||
import CameraImage from './CameraImage';
|
||||
import { ApiHost, Config } from '../context';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
||||
|
||||
const MIN_LOAD_TIMEOUT_MS = 200;
|
||||
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const cameraConfig = config.cameras[camera];
|
||||
|
||||
export default function AutoUpdatingCameraImage({ camera, searchParams, showFps = true }) {
|
||||
const [key, setKey] = useState(Date.now());
|
||||
const [fps, setFps] = useState(0);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
const loadTime = Date.now() - key;
|
||||
setFps((1000 / Math.max(loadTime, MIN_LOAD_TIMEOUT_MS)).toFixed(1));
|
||||
setTimeout(
|
||||
() => {
|
||||
setKey(Date.now());
|
||||
},
|
||||
loadTime > MIN_LOAD_TIMEOUT_MS ? 1 : MIN_LOAD_TIMEOUT_MS
|
||||
);
|
||||
}, [key, searchParams, setFps]);
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setKey(Date.now());
|
||||
}, 500);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [key, searchParams]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CameraImage camera={camera} onload={handleLoad} searchParams={`cache=${key}&${searchParams}`} />
|
||||
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
|
||||
</div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
|
||||
alt={`Auto-updating ${camera} image`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { ApiHost, Config } from '../context';
|
||||
import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks';
|
||||
|
||||
export default function CameraImage({ camera, onload, searchParams = '' }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [availableWidth, setAvailableWidth] = useState(0);
|
||||
const [loadedSrc, setLoadedSrc] = useState(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const { name, width, height } = config.cameras[camera];
|
||||
const aspectRatio = width / height;
|
||||
|
||||
const resizeObserver = useMemo(() => {
|
||||
return new ResizeObserver((entries) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (Array.isArray(entries) && entries.length) {
|
||||
setAvailableWidth(entries[0].contentRect.width);
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [setAvailableWidth, width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}, [resizeObserver, containerRef.current]);
|
||||
|
||||
const scaledHeight = useMemo(() => Math.min(Math.ceil(availableWidth / aspectRatio), height), [
|
||||
availableWidth,
|
||||
aspectRatio,
|
||||
height,
|
||||
]);
|
||||
|
||||
const img = useMemo(() => new Image(), [camera]);
|
||||
img.onload = useCallback(
|
||||
(event) => {
|
||||
const src = event.path[0].currentSrc;
|
||||
setLoadedSrc(src);
|
||||
onload && onload(event);
|
||||
},
|
||||
[searchParams, onload]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scaledHeight) {
|
||||
return;
|
||||
}
|
||||
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
|
||||
}, [apiHost, name, img, searchParams, scaledHeight]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{loadedSrc ? <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,13 @@ import { h } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
|
||||
export default function Switch({ checked, label, id, onChange }) {
|
||||
const handleChange = useCallback(() => {
|
||||
onChange(id, !checked);
|
||||
}, [id, onChange, checked]);
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
console.log(event.target.checked, !checked);
|
||||
onChange(id, !checked);
|
||||
},
|
||||
[id, onChange, checked]
|
||||
);
|
||||
|
||||
return (
|
||||
<label for={id} className="flex items-center cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user