Compare commits

..

7 Commits

Author SHA1 Message Date
Paul Armstrong
0232a9f94a fix(web): ensure all links on events page include pathname 2021-01-25 21:15:25 -06:00
Blake Blackshear
e8586d6459 updating for docusaurus2 docs 2021-01-25 21:13:33 -06:00
Paul Armstrong
9cf49b5bc4 fix(web): make camera latest.jpg responsive 2021-01-25 21:03:46 -06:00
Blake Blackshear
4a18fc58de add search to docs 2021-01-25 21:01:43 -06:00
Blake Blackshear
5ddfde2e72 tweaking the docs 2021-01-24 08:27:43 -06:00
Blake Blackshear
714d76887d use sqlitequeuedb 2021-01-24 06:53:37 -06:00
James Carlos
0c0e1416ff Update documentation link in sidebar to new docs 2021-01-23 07:00:51 -06:00
15 changed files with 117 additions and 37 deletions

View File

@@ -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>
![Events](docs/static/img/events-ui.png)

View File

@@ -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|

View File

@@ -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:
![mismatched-resolution](/img/mismatched-resolution.jpg) ![mismatched-resolution](/img/mismatched-resolution.jpg)
## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found" ## "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -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)

View File

@@ -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]}

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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`}
/>
);
} }

View 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}
/>
);
}

View File

@@ -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">