forked from Github/frigate
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e692acfbb | ||
|
|
a67b8ab84d | ||
|
|
4cf55ad8e2 | ||
|
|
c1132e6897 | ||
|
|
d6104f2eb2 | ||
|
|
b0e0abe385 | ||
|
|
4916e1cd1d | ||
|
|
cd87f3e6f4 | ||
|
|
18f4ab2644 | ||
|
|
0bd3be94ec |
@@ -8,3 +8,4 @@ config/
|
|||||||
core
|
core
|
||||||
*.mp4
|
*.mp4
|
||||||
*.db
|
*.db
|
||||||
|
*.ts
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ debug
|
|||||||
config/config.yml
|
config/config.yml
|
||||||
models
|
models
|
||||||
*.mp4
|
*.mp4
|
||||||
|
*.ts
|
||||||
*.db
|
*.db
|
||||||
frigate/version.py
|
frigate/version.py
|
||||||
web/build
|
web/build
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@ default_target: amd64_frigate
|
|||||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||||
|
|
||||||
version:
|
version:
|
||||||
echo "VERSION='0.9.2-$(COMMIT_HASH)'" > frigate/version.py
|
echo "VERSION='0.9.3-$(COMMIT_HASH)'" > frigate/version.py
|
||||||
|
|
||||||
web:
|
web:
|
||||||
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
docker build --tag frigate-web --file docker/Dockerfile.web web/
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ Note that mjpeg cameras require encoding the video into h264 for recording, and
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
output_args:
|
output_args:
|
||||||
record: -f segment -segment_time 10 -segment_format ts -reset_timestamps 1 -strftime 1 -c:v libx264
|
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
|
||||||
rtmp: -c:v libx264 -f flv
|
rtmp: -c:v libx264 -an -f flv
|
||||||
```
|
```
|
||||||
|
|
||||||
### RTMP Cameras
|
### RTMP Cameras
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ mqtt:
|
|||||||
# Optional: user
|
# Optional: user
|
||||||
user: mqtt_user
|
user: mqtt_user
|
||||||
# Optional: password
|
# Optional: password
|
||||||
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
|
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
|
||||||
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
|
# e.g. password: '{FRIGATE_MQTT_PASSWORD}'
|
||||||
password: password
|
password: password
|
||||||
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
|
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
|
||||||
tls_ca_certs: /path/to/ca.crt
|
tls_ca_certs: /path/to/ca.crt
|
||||||
@@ -140,7 +140,7 @@ ffmpeg:
|
|||||||
# Optional: output args for detect streams (default: shown below)
|
# Optional: output args for detect streams (default: shown below)
|
||||||
detect: -f rawvideo -pix_fmt yuv420p
|
detect: -f rawvideo -pix_fmt yuv420p
|
||||||
# Optional: output args for record streams (default: shown below)
|
# Optional: output args for record streams (default: shown below)
|
||||||
record: -f segment -segment_time 10 -segment_format ts -reset_timestamps 1 -strftime 1 -c copy
|
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||||
# Optional: output args for rtmp streams (default: shown below)
|
# Optional: output args for rtmp streams (default: shown below)
|
||||||
rtmp: -c copy -f flv
|
rtmp: -c copy -f flv
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ cameras:
|
|||||||
# Required: A list of input streams for the camera. See documentation for more information.
|
# Required: A list of input streams for the camera. See documentation for more information.
|
||||||
inputs:
|
inputs:
|
||||||
# Required: the path to the stream
|
# Required: the path to the stream
|
||||||
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}
|
# NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {}
|
||||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||||
# Required: list of roles for this stream. valid values are: detect,record,rtmp
|
# Required: list of roles for this stream. valid values are: detect,record,rtmp
|
||||||
# NOTICE: In addition to assigning the record, and rtmp roles,
|
# NOTICE: In addition to assigning the record, and rtmp roles,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ A solid green image means that frigate has not received any frames from ffmpeg.
|
|||||||
|
|
||||||
### How can I get sound or audio in my recordings?
|
### How can I get sound or audio in my recordings?
|
||||||
|
|
||||||
The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
|
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](configuration/index#full-configuration-reference).
|
||||||
|
|
||||||
### My mjpeg stream or snapshots look green and crazy
|
### My mjpeg stream or snapshots look green and crazy
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,61 @@ Frigate runs best with docker installed on bare metal debian-based distributions
|
|||||||
|
|
||||||
Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help.
|
Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help.
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
|
||||||
|
|
||||||
|
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||||
|
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||||
|
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
|
||||||
|
- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder.
|
||||||
|
- `/dev/shm`: It is not recommended to modify this directory or map it with docker. This is the location for raw decoded frames in shared memory and it's size is impacted by the `shm-size` calculations below.
|
||||||
|
- `/config/config.yml`: Default location of the config file.
|
||||||
|
|
||||||
|
#### Common docker compose storage configurations
|
||||||
|
|
||||||
|
Writing to a local disk or external USB drive:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
frigate:
|
||||||
|
...
|
||||||
|
volumes:
|
||||||
|
- /path/to/your/config.yml:/config/config.yml:ro
|
||||||
|
- /path/to/your/storage:/media/frigate
|
||||||
|
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||||
|
target: /tmp/cache
|
||||||
|
tmpfs:
|
||||||
|
size: 1000000000
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Writing to a network drive with database on a local drive:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
frigate:
|
||||||
|
...
|
||||||
|
volumes:
|
||||||
|
- /path/to/your/config.yml:/config/config.yml:ro
|
||||||
|
- /path/to/network/storage:/media/frigate
|
||||||
|
- /path/to/local/disk:/db
|
||||||
|
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||||
|
target: /tmp/cache
|
||||||
|
tmpfs:
|
||||||
|
size: 1000000000
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
frigate.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
database:
|
||||||
|
path: /db/frigate.db
|
||||||
|
```
|
||||||
|
|
||||||
### Calculating required shm-size
|
### Calculating required shm-size
|
||||||
|
|
||||||
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
|
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ module.exports = {
|
|||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
position: 'left',
|
position: 'left',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: 'https://frigate.video',
|
||||||
|
label: 'Website',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'https://demo.frigate.video',
|
||||||
|
label: 'Demo',
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: 'https://github.com/blakeblackshear/frigate',
|
href: 'https://github.com/blakeblackshear/frigate',
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
|
|||||||
@@ -298,13 +298,14 @@ RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
|||||||
"-segment_time",
|
"-segment_time",
|
||||||
"10",
|
"10",
|
||||||
"-segment_format",
|
"-segment_format",
|
||||||
"ts",
|
"mp4",
|
||||||
"-reset_timestamps",
|
"-reset_timestamps",
|
||||||
"1",
|
"1",
|
||||||
"-strftime",
|
"-strftime",
|
||||||
"1",
|
"1",
|
||||||
"-c",
|
"-c",
|
||||||
"copy",
|
"copy",
|
||||||
|
"-an",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -564,16 +565,9 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
else self.ffmpeg.output_args.record.split(" ")
|
else self.ffmpeg.output_args.record.split(" ")
|
||||||
)
|
)
|
||||||
|
|
||||||
# backwards compatibility check for segment_format change from mp4 to ts
|
|
||||||
record_args = (
|
|
||||||
" ".join(record_args)
|
|
||||||
.replace("-segment_format mp4", "-segment_format ts")
|
|
||||||
.split(" ")
|
|
||||||
)
|
|
||||||
|
|
||||||
ffmpeg_output_args = (
|
ffmpeg_output_args = (
|
||||||
record_args
|
record_args
|
||||||
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.ts"]
|
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
|
||||||
+ ffmpeg_output_args
|
+ ffmpeg_output_args
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -800,19 +794,8 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
config.cameras[name] = camera_config
|
config.cameras[name] = camera_config
|
||||||
|
|
||||||
return config
|
# check runtime config
|
||||||
|
for name, camera in config.cameras.items():
|
||||||
@validator("cameras")
|
|
||||||
def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
|
|
||||||
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
|
|
||||||
for zone in zones:
|
|
||||||
if zone in v.keys():
|
|
||||||
raise ValueError("Zones cannot share names with cameras")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator("cameras")
|
|
||||||
def ensure_cameras_are_not_missing_roles(cls, v: Dict[str, CameraConfig]):
|
|
||||||
for name, camera in v.items():
|
|
||||||
assigned_roles = list(
|
assigned_roles = list(
|
||||||
set([r for i in camera.ffmpeg.inputs for r in i.roles])
|
set([r for i in camera.ffmpeg.inputs for r in i.roles])
|
||||||
)
|
)
|
||||||
@@ -825,6 +808,15 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
|
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
@validator("cameras")
|
||||||
|
def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
|
||||||
|
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
|
||||||
|
for zone in zones:
|
||||||
|
if zone in v.keys():
|
||||||
|
raise ValueError("Zones cannot share names with cameras")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
recordings = [
|
recordings = [
|
||||||
d
|
d
|
||||||
for d in os.listdir(CACHE_DIR)
|
for d in os.listdir(CACHE_DIR)
|
||||||
if os.path.isfile(os.path.join(CACHE_DIR, d)) and d.endswith(".ts")
|
if os.path.isfile(os.path.join(CACHE_DIR, d))
|
||||||
|
and d.endswith(".mp4")
|
||||||
|
and not d.startswith("clip_")
|
||||||
]
|
]
|
||||||
|
|
||||||
files_in_use = []
|
files_in_use = []
|
||||||
@@ -111,30 +113,9 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
||||||
file_path = os.path.join(directory, file_name)
|
file_path = os.path.join(directory, file_name)
|
||||||
|
|
||||||
ffmpeg_cmd = [
|
# copy then delete is required when recordings are stored on some network drives
|
||||||
"ffmpeg",
|
shutil.copyfile(cache_path, file_path)
|
||||||
"-y",
|
os.remove(cache_path)
|
||||||
"-i",
|
|
||||||
cache_path,
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
"-movflags",
|
|
||||||
"+faststart",
|
|
||||||
file_path,
|
|
||||||
]
|
|
||||||
|
|
||||||
p = sp.run(
|
|
||||||
ffmpeg_cmd,
|
|
||||||
encoding="ascii",
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
logger.error(f"Unable to convert {cache_path} to {file_path}")
|
|
||||||
logger.error(p.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
rand_id = "".join(
|
rand_id = "".join(
|
||||||
random.choices(string.ascii_lowercase + string.digits, k=6)
|
random.choices(string.ascii_lowercase + string.digits, k=6)
|
||||||
@@ -150,8 +131,11 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# Check for new files every 5 seconds
|
# Check for new files every 5 seconds
|
||||||
while not self.stop_event.wait(5):
|
wait_time = 5
|
||||||
|
while not self.stop_event.wait(wait_time):
|
||||||
|
run_start = datetime.datetime.now().timestamp()
|
||||||
self.move_files()
|
self.move_files()
|
||||||
|
wait_time = max(0, 5 - (datetime.datetime.now().timestamp() - run_start))
|
||||||
|
|
||||||
logger.info(f"Exiting recording maintenance...")
|
logger.info(f"Exiting recording maintenance...")
|
||||||
|
|
||||||
@@ -290,9 +274,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
|
|
||||||
# find all the recordings older than the oldest recording in the db
|
# find all the recordings older than the oldest recording in the db
|
||||||
try:
|
try:
|
||||||
oldest_recording = (
|
oldest_recording = Recordings.select().order_by(Recordings.start_time).get()
|
||||||
Recordings.select().order_by(Recordings.start_time.desc()).get()
|
|
||||||
)
|
|
||||||
|
|
||||||
p = Path(oldest_recording.path)
|
p = Path(oldest_recording.path)
|
||||||
oldest_timestamp = p.stat().st_mtime - 1
|
oldest_timestamp = p.stat().st_mtime - 1
|
||||||
@@ -301,7 +283,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
|
|
||||||
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
||||||
process = sp.run(
|
process = sp.run(
|
||||||
["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"],
|
["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -702,7 +702,11 @@ class TestConfig(unittest.TestCase):
|
|||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"path": "rtsp://10.0.0.1:554/video",
|
"path": "rtsp://10.0.0.1:554/video",
|
||||||
"roles": ["detect", "clips"],
|
"roles": ["detect"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video2",
|
||||||
|
"roles": ["clips"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -717,6 +721,37 @@ class TestConfig(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
|
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
|
||||||
|
|
||||||
|
def test_fails_on_missing_role(self):
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"cameras": {
|
||||||
|
"back": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video",
|
||||||
|
"roles": ["detect"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video2",
|
||||||
|
"roles": ["record"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"height": 1080,
|
||||||
|
"width": 1920,
|
||||||
|
"fps": 5,
|
||||||
|
},
|
||||||
|
"rtmp": {"enabled": True},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate_config = FrigateConfig(**config)
|
||||||
|
self.assertRaises(ValueError, lambda: frigate_config.runtime_config)
|
||||||
|
|
||||||
def test_global_detect(self):
|
def test_global_detect(self):
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@@ -958,6 +993,34 @@ class TestConfig(unittest.TestCase):
|
|||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config
|
||||||
assert runtime_config.cameras["back"].rtmp.enabled
|
assert runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
|
def test_global_rtmp_default(self):
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"rtmp": {"enabled": False},
|
||||||
|
"cameras": {
|
||||||
|
"back": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video",
|
||||||
|
"roles": ["detect"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video2",
|
||||||
|
"roles": ["record"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
frigate_config = FrigateConfig(**config)
|
||||||
|
assert config == frigate_config.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
runtime_config = frigate_config.runtime_config
|
||||||
|
assert not runtime_config.cameras["back"].rtmp.enabled
|
||||||
|
|
||||||
def test_global_live(self):
|
def test_global_live(self):
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user