forked from Github/frigate
Compare commits
5 Commits
v0.8.0-rc5
...
v0.8.0-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0232a9f94a | ||
|
|
e8586d6459 | ||
|
|
9cf49b5bc4 | ||
|
|
4a18fc58de | ||
|
|
5ddfde2e72 |
16
README.md
16
README.md
@@ -14,9 +14,25 @@ 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,6 +21,7 @@ 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,7 +6,6 @@ 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,6 +9,10 @@ module.exports = {
|
||||
organizationName: 'blakeblackshear',
|
||||
projectName: 'frigate',
|
||||
themeConfig: {
|
||||
algolia: {
|
||||
apiKey: '81ec882db78f7fed05c51daf973f0362',
|
||||
indexName: 'frigate'
|
||||
},
|
||||
navbar: {
|
||||
title: 'Frigate',
|
||||
logo: {
|
||||
|
||||
BIN
docs/static/img/camera-ui.png
vendored
Normal file
BIN
docs/static/img/camera-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 944 KiB |
BIN
docs/static/img/events-ui.png
vendored
Normal file
BIN
docs/static/img/events-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/home-ui.png
vendored
Normal file
BIN
docs/static/img/home-ui.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
@@ -1,6 +1,7 @@
|
||||
import { h } from 'preact';
|
||||
import Box from './components/Box';
|
||||
import Button from './components/Button';
|
||||
import CameraImage from './components/CameraImage';
|
||||
import Heading from './components/Heading';
|
||||
import Switch from './components/Switch';
|
||||
import { route } from 'preact-router';
|
||||
@@ -27,14 +28,26 @@ 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;
|
||||
}
|
||||
const scaledWidth = imageRef.current.width;
|
||||
const scale = scaledWidth / width;
|
||||
setImageScale(scale);
|
||||
}, [imageRef.current, setImageScale]);
|
||||
resizeObserver.observe(imageRef.current);
|
||||
}, [resizeObserver, imageRef.current]);
|
||||
|
||||
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
||||
Array.isArray(motionMask)
|
||||
@@ -226,7 +239,7 @@ ${Object.keys(objectMaskPoints)
|
||||
|
||||
<Box className="space-y-4">
|
||||
<div className="relative">
|
||||
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
|
||||
<CameraImage imageRef={imageRef} camera={camera} />
|
||||
<EditableMask
|
||||
onChange={handleUpdateEditable}
|
||||
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -23,7 +24,6 @@ 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>
|
||||
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
||||
<CameraImage camera={name} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Events({ url } = {}) {
|
||||
const apiHost = useContext(ApiHost);
|
||||
const [events, setEvents] = useState([]);
|
||||
|
||||
const searchParams = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`).searchParams;
|
||||
const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`);
|
||||
const searchParamsString = searchParams.toString();
|
||||
|
||||
useEffect(async () => {
|
||||
@@ -32,9 +32,10 @@ export default function Events({ url } = {}) {
|
||||
<div className="flex flex-wrap space-x-2">
|
||||
{searchKeys.map((filterKey) => (
|
||||
<UnFilterable
|
||||
paramName={filterKey}
|
||||
searchParams={searchParamsString}
|
||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
||||
paramName={filterKey}
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -71,17 +72,32 @@ export default function Events({ url } = {}) {
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="camera"
|
||||
name={camera}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="label"
|
||||
name={label}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||
<Td>
|
||||
<ul>
|
||||
{zones.map((zone) => (
|
||||
<li>
|
||||
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
|
||||
<Filterable
|
||||
pathname={pathname}
|
||||
searchParams={searchParamsString}
|
||||
paramName="zone"
|
||||
name={zone}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -100,19 +116,19 @@ export default function Events({ url } = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
function Filterable({ searchParams, paramName, name }) {
|
||||
function Filterable({ pathname, searchParams, paramName, name }) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set(paramName, name);
|
||||
return <Link href={`?${params.toString()}`}>{name}</Link>;
|
||||
return <Link href={`${pathname}?${params.toString()}`}>{name}</Link>;
|
||||
}
|
||||
|
||||
function UnFilterable({ searchParams, paramName, name }) {
|
||||
function UnFilterable({ pathname, 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={`?${params.toString()}`}
|
||||
href={`${pathname}?${params.toString()}`}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
import CameraImage from './CameraImage';
|
||||
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(() => {
|
||||
@@ -17,11 +16,5 @@ export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
||||
};
|
||||
}, [key, searchParams]);
|
||||
|
||||
return (
|
||||
<img
|
||||
className="w-full"
|
||||
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
|
||||
alt={`Auto-updating ${camera} image`}
|
||||
/>
|
||||
);
|
||||
return <CameraImage camera={camera} searchParams={`cache=${key}&${searchParams}`} />;
|
||||
}
|
||||
|
||||
38
web/src/components/CameraImage.jsx
Normal file
38
web/src/components/CameraImage.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { h } from 'preact';
|
||||
import { ApiHost, Config } from '../context';
|
||||
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
||||
|
||||
export default function CameraImage({ camera, searchParams = '', imageRef }) {
|
||||
const config = useContext(Config);
|
||||
const apiHost = useContext(ApiHost);
|
||||
const { name, width, height } = config.cameras[camera];
|
||||
|
||||
const aspectRatio = width / height;
|
||||
const innerWidth = parseInt(window.innerWidth, 10);
|
||||
|
||||
const responsiveWidths = [640, 768, 1024, 1280];
|
||||
if (innerWidth > responsiveWidths[responsiveWidths.length - 1]) {
|
||||
responsiveWidths.push(innerWidth);
|
||||
}
|
||||
|
||||
const src = `${apiHost}/api/${camera}/latest.jpg`;
|
||||
const { srcset, sizes } = responsiveWidths.reduce(
|
||||
(memo, w, i) => {
|
||||
memo.srcset.push(`${src}?h=${Math.ceil(w / aspectRatio)}&${searchParams} ${w}w`);
|
||||
memo.sizes.push(`(max-width: ${w}) ${Math.ceil((w / innerWidth) * 100)}vw`);
|
||||
return memo;
|
||||
},
|
||||
{ srcset: [], sizes: [] }
|
||||
);
|
||||
|
||||
return (
|
||||
<img
|
||||
className="w-full"
|
||||
srcset={srcset.join(', ')}
|
||||
sizes={sizes.join(', ')}
|
||||
src={`${srcset[srcset.length - 1]}`}
|
||||
alt={name}
|
||||
ref={imageRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,9 @@ import { h } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
|
||||
export default function Switch({ checked, label, id, onChange }) {
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
console.log(event.target.checked, !checked);
|
||||
onChange(id, !checked);
|
||||
},
|
||||
[id, onChange, checked]
|
||||
);
|
||||
const handleChange = useCallback(() => {
|
||||
onChange(id, !checked);
|
||||
}, [id, onChange, checked]);
|
||||
|
||||
return (
|
||||
<label for={id} className="flex items-center cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user