forked from Github/frigate
Compare commits
7 Commits
v0.8.0-rc4
...
v0.8.0-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0232a9f94a | ||
|
|
e8586d6459 | ||
|
|
9cf49b5bc4 | ||
|
|
4a18fc58de | ||
|
|
5ddfde2e72 | ||
|
|
714d76887d | ||
|
|
0c0e1416ff |
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
|
- 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
|
- Object detection with TensorFlow runs in separate processes for maximum FPS
|
||||||
- Communicates over MQTT for easy integration into other systems
|
- Communicates over MQTT for easy integration into other systems
|
||||||
|
- Records video clips of detected objects
|
||||||
- 24/7 recording
|
- 24/7 recording
|
||||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
View the documentation at https://blakeblackshear.github.io/frigate
|
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
|
## Docker
|
||||||
|
|
||||||
Make sure you choose the right image for your architecture:
|
Make sure you choose the right image for your architecture:
|
||||||
|
|
||||||
|Arch|Image Name|
|
|Arch|Image Name|
|
||||||
|-|-|
|
|-|-|
|
||||||
|amd64|blakeblackshear/frigate:stable-amd64|
|
|amd64|blakeblackshear/frigate:stable-amd64|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ title: Troubleshooting
|
|||||||
### My mjpeg stream or snapshots look green and crazy
|
### 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.
|
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"
|
## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ module.exports = {
|
|||||||
organizationName: 'blakeblackshear',
|
organizationName: 'blakeblackshear',
|
||||||
projectName: 'frigate',
|
projectName: 'frigate',
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
algolia: {
|
||||||
|
apiKey: '81ec882db78f7fed05c51daf973f0362',
|
||||||
|
indexName: 'frigate'
|
||||||
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'Frigate',
|
title: 'Frigate',
|
||||||
logo: {
|
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 |
@@ -10,6 +10,7 @@ import signal
|
|||||||
import yaml
|
import yaml
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
|
from playhouse.sqliteq import SqliteQueueDatabase
|
||||||
|
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||||
@@ -117,13 +118,16 @@ class FrigateApp():
|
|||||||
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
|
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
|
||||||
|
|
||||||
def init_database(self):
|
def init_database(self):
|
||||||
self.db = SqliteExtDatabase(self.config.database.path)
|
migrate_db = SqliteExtDatabase(self.config.database.path)
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
del(logging.getLogger('peewee_migrate').handlers[:])
|
del(logging.getLogger('peewee_migrate').handlers[:])
|
||||||
router = Router(self.db)
|
router = Router(migrate_db)
|
||||||
router.run()
|
router.run()
|
||||||
|
|
||||||
|
migrate_db.close()
|
||||||
|
|
||||||
|
self.db = SqliteQueueDatabase(self.config.database.path)
|
||||||
models = [Event]
|
models = [Event]
|
||||||
self.db.bind(models)
|
self.db.bind(models)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
import Button from './components/Button';
|
import Button from './components/Button';
|
||||||
|
import CameraImage from './components/CameraImage';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
import Switch from './components/Switch';
|
import Switch from './components/Switch';
|
||||||
import { route } from 'preact-router';
|
import { route } from 'preact-router';
|
||||||
@@ -27,14 +28,26 @@ export default function CameraMasks({ camera, url }) {
|
|||||||
zones,
|
zones,
|
||||||
} = cameraConfig;
|
} = 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(() => {
|
useEffect(() => {
|
||||||
if (!imageRef.current) {
|
if (!imageRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scaledWidth = imageRef.current.width;
|
resizeObserver.observe(imageRef.current);
|
||||||
const scale = scaledWidth / width;
|
}, [resizeObserver, imageRef.current]);
|
||||||
setImageScale(scale);
|
|
||||||
}, [imageRef.current, setImageScale]);
|
|
||||||
|
|
||||||
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
const [motionMaskPoints, setMotionMaskPoints] = useState(
|
||||||
Array.isArray(motionMask)
|
Array.isArray(motionMask)
|
||||||
@@ -226,7 +239,7 @@ ${Object.keys(objectMaskPoints)
|
|||||||
|
|
||||||
<Box className="space-y-4">
|
<Box className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
|
<CameraImage imageRef={imageRef} camera={camera} />
|
||||||
<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]}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import Box from './components/Box';
|
import Box from './components/Box';
|
||||||
|
import CameraImage from './components/CameraImage';
|
||||||
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';
|
||||||
@@ -23,7 +24,6 @@ export default function Cameras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Camera({ name }) {
|
function Camera({ name }) {
|
||||||
const apiHost = useContext(ApiHost);
|
|
||||||
const href = `/cameras/${name}`;
|
const href = `/cameras/${name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +32,7 @@ function Camera({ name }) {
|
|||||||
href={href}
|
href={href}
|
||||||
>
|
>
|
||||||
<Heading size="base">{name}</Heading>
|
<Heading size="base">{name}</Heading>
|
||||||
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
|
<CameraImage camera={name} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function Events({ url } = {}) {
|
|||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useContext(ApiHost);
|
||||||
const [events, setEvents] = useState([]);
|
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();
|
const searchParamsString = searchParams.toString();
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
@@ -32,9 +32,10 @@ export default function Events({ url } = {}) {
|
|||||||
<div className="flex flex-wrap space-x-2">
|
<div className="flex flex-wrap space-x-2">
|
||||||
{searchKeys.map((filterKey) => (
|
{searchKeys.map((filterKey) => (
|
||||||
<UnFilterable
|
<UnFilterable
|
||||||
paramName={filterKey}
|
|
||||||
searchParams={searchParamsString}
|
|
||||||
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
name={`${filterKey}: ${searchParams.get(filterKey)}`}
|
||||||
|
paramName={filterKey}
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParamsString}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,17 +72,32 @@ export default function Events({ url } = {}) {
|
|||||||
</a>
|
</a>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
|
<Filterable
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParamsString}
|
||||||
|
paramName="camera"
|
||||||
|
name={camera}
|
||||||
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
|
<Filterable
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParamsString}
|
||||||
|
paramName="label"
|
||||||
|
name={label}
|
||||||
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<ul>
|
<ul>
|
||||||
{zones.map((zone) => (
|
{zones.map((zone) => (
|
||||||
<li>
|
<li>
|
||||||
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
|
<Filterable
|
||||||
|
pathname={pathname}
|
||||||
|
searchParams={searchParamsString}
|
||||||
|
paramName="zone"
|
||||||
|
name={zone}
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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);
|
const params = new URLSearchParams(searchParams);
|
||||||
params.set(paramName, name);
|
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);
|
const params = new URLSearchParams(searchParams);
|
||||||
params.delete(paramName);
|
params.delete(paramName);
|
||||||
return (
|
return (
|
||||||
<a
|
<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"
|
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}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function Sidebar() {
|
|||||||
<hr className="border-solid border-gray-500 mt-2" />
|
<hr className="border-solid border-gray-500 mt-2" />
|
||||||
<NavLink
|
<NavLink
|
||||||
className="self-end"
|
className="self-end"
|
||||||
href="https://github.com/blakeblackshear/frigate/blob/master/README.md"
|
href="https://blakeblackshear.github.io/frigate"
|
||||||
text="Documentation"
|
text="Documentation"
|
||||||
/>
|
/>
|
||||||
<NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
|
<NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import CameraImage from './CameraImage';
|
||||||
import { ApiHost, Config } from '../context';
|
import { ApiHost, Config } from '../context';
|
||||||
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
||||||
const config = useContext(Config);
|
|
||||||
const apiHost = useContext(ApiHost);
|
const apiHost = useContext(ApiHost);
|
||||||
const cameraConfig = config.cameras[camera];
|
|
||||||
|
|
||||||
const [key, setKey] = useState(Date.now());
|
const [key, setKey] = useState(Date.now());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -17,11 +16,5 @@ export default function AutoUpdatingCameraImage({ camera, searchParams }) {
|
|||||||
};
|
};
|
||||||
}, [key, searchParams]);
|
}, [key, searchParams]);
|
||||||
|
|
||||||
return (
|
return <CameraImage camera={camera} searchParams={`cache=${key}&${searchParams}`} />;
|
||||||
<img
|
|
||||||
className="w-full"
|
|
||||||
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
|
|
||||||
alt={`Auto-updating ${camera} image`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
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';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function Switch({ checked, label, id, onChange }) {
|
export default function Switch({ checked, label, id, onChange }) {
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(() => {
|
||||||
(event) => {
|
onChange(id, !checked);
|
||||||
console.log(event.target.checked, !checked);
|
}, [id, onChange, checked]);
|
||||||
onChange(id, !checked);
|
|
||||||
},
|
|
||||||
[id, onChange, checked]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label for={id} className="flex items-center cursor-pointer">
|
<label for={id} className="flex items-center cursor-pointer">
|
||||||
|
|||||||
Reference in New Issue
Block a user