Compare commits

...

293 Commits

Author SHA1 Message Date
Blake Blackshear
37325c70ba fix plus enabled for addons 2022-07-22 08:12:58 -05:00
Blake Blackshear
3c46a33992 revert false warning messages 2022-07-20 06:55:06 -05:00
deviant77
ed1897db71 Add log message when discarding recording segments in cache (#3439)
* Add log message when discarding recording segments in cache

Currently Frigate silently discards recording segments in cache if there's more than "keep_count" for a camera, which can happen on high load/busy/slow systems.
This results in recording segments being lost with no apparent cause in the logs (even when set to debug).
This PR adds a warning log entry when discarding segments, in the same way as discarding corrupted segments

* Add explanatory warning and properly format cache_path warning

* lint fixes

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2022-07-19 07:24:44 -05:00
Caros2017
dfbebb63ff Update hardware_acceleration.md
Added the solution for the problem in https://github.com/blakeblackshear/frigate/issues/3227
2022-07-19 07:06:40 -05:00
JohnMark Sill
a67a768e89 improvement: better play/pause 2022-07-19 07:04:33 -05:00
JohnMark Sill
43f05c18d6 chore: remove unused import 2022-07-19 07:04:33 -05:00
JohnMark Sill
3b076c28c2 chore: removed unused properties interface 2022-07-19 07:04:33 -05:00
JohnMark Sill
cbf12e3f90 fix: removed unused state 2022-07-19 07:04:33 -05:00
JohnMark Sill
17b745434c improvement: migrated to videojs 2022-07-19 07:04:33 -05:00
JohnMark Sill
37011c2fda improvement: use useCallback instead of setting ref auto-magically 2022-07-19 07:04:33 -05:00
JohnMark Sill
fa95a041dd fix: height of video is now constant in history viewer 2022-07-19 07:04:33 -05:00
JohnMark Sill
0879d7a2d1 fix: marker time image 2022-07-19 07:04:33 -05:00
Nick Mowen
061fb15a80 Reset motion to false on startup 2022-07-19 06:59:20 -05:00
Blake Blackshear
3246fcce22 document buildx setup 2022-07-19 06:56:23 -05:00
Blake Blackshear
f2a3797b46 add additional render group 2022-07-19 06:44:11 -05:00
Blake Blackshear
b80080ac52 don't refetch data on refocus 2022-07-07 07:05:05 -05:00
Blake Blackshear
b36b63599b update stimeout to timeout 2022-07-05 11:55:55 -05:00
Blake Blackshear
5d8c0e43c2 try ffmpeg5 again 2022-07-05 11:54:21 -05:00
Josh Hawkins
7845995dfd Adjust threshold and contour_area with mqtt 2022-07-05 08:46:10 -05:00
Blake Blackshear
afe88d6e3a switch back to upgraded numpy 2022-07-04 16:51:48 -05:00
Blake Blackshear
560ee0104d arm32 compat 2022-07-04 09:06:26 -05:00
Blake Blackshear
dc8b625d55 sync numpy version 2022-07-03 10:12:14 -05:00
Blake Blackshear
162c0147d2 use jellyfin for all arch 2022-07-03 10:12:00 -05:00
Blake Blackshear
ef54cd6fb3 add plus endpoint to docs 2022-07-01 06:57:03 -05:00
Nicolas Mowen
c2465a46a8 Http tests (#3350)
* Set up for http tests

* Setup basics for testing and first test

* Add testing consts

* Cleanup db creation

* Add one more check to test

* Get event that does not exist

* Get events working with cleaner db

* Test retain / un-retain

* Test setting and deleting sub label

* Test getting list of sub labels

* Fix bug caught in tests

* Test deleting event

* Test geting list of events

* Expand test

* Test more event filters

* Write version module so tests don't fail on version import

* Test config

* Test recordings endpoint

* Formatting

* Remove unused imports

* Test stats

* Add cleanup files in const

* Add name to match other checks
2022-06-30 07:53:46 -05:00
Blake Blackshear
24d3a9cdd5 read plus api key from addon options 2022-06-30 07:19:40 -05:00
Blake Blackshear
5e82eaed88 switch back to ffmpeg 4.4.1 2022-06-28 06:50:19 -05:00
Blake Blackshear
1d45b0b351 update hwaccel docs 2022-06-11 06:42:09 -05:00
Blake Blackshear
ba119e4f96 revert back to ffmpeg 4.3.2 2022-06-10 07:38:26 -05:00
Blake Blackshear
75c2feb387 ffmpeg5 and intel va driver 2022-06-09 07:16:21 -05:00
Nicolas Mowen
da637d3c8f Limit size of player in events view (#3288)
* Set max width

* Set data-options so videojs accepts size changing

* Add comment to explain exmpty data-setup value

* Clarify comment
2022-06-02 07:06:39 -05:00
Blake Blackshear
bc078fcc88 remove deprecated max_seconds config option 2022-06-01 06:50:09 -05:00
Nicolas Mowen
5f9d477863 Update event filters naming and add sub label filter (#3194)
* Use default names so filters are more clear

* Add endpoint to get list of sub labels inside DB

* Fix crash on no internet

* Cleanups for sub_label http

* Add sub label selector to events UI

* Add event filtering for sub label

* Formatting files

* Reduce size of filters to fit on one line

* Add handler for tests

* Remove unused imports

* Only show the sub labels filter when there are sub labels in the DB

* Fix tests

* Use distinct instead of group_by

* Formatting

* Cleanup event logic
2022-05-29 09:47:43 -05:00
Nicolas Mowen
ca693240b1 Favorite events delete button warning (#3225)
* Add dialog to shield deletion of favorite event

* Use state to keep track of event id

* Adjust named

* Set color of button
2022-05-26 10:06:02 -05:00
Nicolas Mowen
468febc434 Catch crash on no internet (#3246) 2022-05-26 10:04:33 -05:00
Blake Blackshear
4b81c88794 use specific jellyfin-ffmpeg build 2022-05-26 10:02:43 -05:00
Blake Blackshear
2ac28b93f3 fix development port 2022-05-20 09:28:26 -05:00
Blake Blackshear
3e7ed982d4 fix tests 2022-05-20 09:28:26 -05:00
Nicolas Mowen
d8d410802f Set height and width of delete icon for firefox (#3226) 2022-05-19 07:41:31 -05:00
Blake Blackshear
ca7bad8909 get ingress to play nice with vite 2022-05-19 07:31:51 -05:00
Patrick Fruh
7b2b5bfa71 update hwaccel docs for nvidia docker-compose 2022-05-18 17:19:37 -05:00
Blake Blackshear
d2c3cdcf04 don't add 16x on every render 2022-05-16 07:00:33 -05:00
Blake Blackshear
7c8142174e mqtt motion adjustments 2022-05-15 07:45:04 -05:00
Blake Blackshear
efe3f96223 update hwaccel docs for RPi 2022-05-15 07:19:55 -05:00
Nicolas Mowen
de244d6873 Send mqtt message when motion is detected (#3152)
* Send mqtt message when motion is detected

* Use object processing instead of passing mqtt client around

* Cleanup

* Formatting

* add comment

* Make off delay configurable.

* Handle updating each camera based on config off delay

* Formatting

* Update docker-compose.yml

* Fix processing issue

* Update mqtt docs

* Update main config docs

* Make sure multiple True values aren't published for the same motion

* Make sure multiple True values aren't published for the same motion

* Update payload to fit existing HA standard values

* Update docs to fit new values

* Update docs

* Update motion topic

* Use datetime.datetime and remove unused imports

* Cast to int

* Clarify motion detector behavior in docs

* Fix typo

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2022-05-15 07:03:33 -05:00
Carlos Gustavo Sarmiento
90bff605fa Only include 16x if browser is not Firefox 2022-05-14 07:31:52 -05:00
Carlos Gustavo Sarmiento
f512df44c1 Add 16x Playback rate to VideoPlayer.jsx 2022-05-14 07:31:52 -05:00
Blake Blackshear
4e8ce28948 center portrait thumbnails 2022-05-14 06:37:06 -05:00
Blake Blackshear
16057cdef7 only use jellyfin-ffmpeg for amd64 2022-05-14 06:36:38 -05:00
Blake Blackshear
da36f25fd0 fix swr fetcher 2022-05-12 12:05:34 -05:00
Blake Blackshear
0e9c8a4ddd update docusaurus 2022-05-12 06:29:43 -05:00
Blake Blackshear
ccc9b90625 update docs 2022-05-12 06:29:43 -05:00
Blake Blackshear
d992a959f2 sync lint settings 2022-05-12 06:29:43 -05:00
Blake Blackshear
358d0521a1 fix plus active check and add logging 2022-05-12 06:29:43 -05:00
Blake Blackshear
691ed6a4c7 revamp recordings 2022-05-12 06:29:43 -05:00
Blake Blackshear
78e1782084 optimize query performance 2022-05-12 06:29:43 -05:00
Blake Blackshear
0d5ae9a399 cleanup 2022-05-12 06:29:43 -05:00
Blake Blackshear
d343a658ec batch deletes for massive cleanups 2022-05-12 06:29:43 -05:00
Nicolas Mowen
0bd3cff13b Set event thumbnail cache timeout to 1 day (#3150)
* Set event thumbnail cache timeout to 1 day

* Pass max age as a param and set default to 30 days
2022-04-26 19:42:07 -05:00
Nicolas Mowen
5b71271b3c Set motion switch on mqtt connection (#3149) 2022-04-26 19:09:53 -05:00
Nicolas Mowen
0a4d658c7f Ability to enable / disable motion detection via MQTT (#3117)
* Starting working on adding motion toggle

* Add all info to mqtt command

* Send motion to correct funs

* Update mqtt docs

* Fixes for contingencies

* format

* mypy

* Tweak behavior

* Fix motion breaking frames

* Fix bad logic in detect set

* Always set value for motion boxes
2022-04-26 07:29:28 -05:00
Blake Blackshear
b75929a846 add all jellyfin-ffmpeg binaries to path 2022-04-26 07:24:30 -05:00
Blake Blackshear
e33cd442cd ensure params get passed to api for jpg 2022-04-26 07:24:30 -05:00
Blake Blackshear
91539de3ff optimize caching of image data from api 2022-04-26 06:36:26 -05:00
Blake Blackshear
bc0206de9d retain the set topic for switches 2022-04-25 07:13:22 -05:00
Blake Blackshear
f536494a38 Multi arch image with nvidia decode support
* build working

* update makefile

* use jellyfin-ffmpeg for all arch

* just build web once for all arch

* update actions build

* update docs
2022-04-24 13:52:12 -05:00
Blake Blackshear
f2030d301f type fixes 2022-04-18 06:52:13 -05:00
Blake Blackshear
d890217447 Merge remote-tracking branch 'origin/master' into release-0.11.0 2022-04-18 06:45:30 -05:00
Sebastian Englbrecht
cafe0917c7 Typing: mypy fixes for
* __main__.py
 * app.py
 * models.py
 * plus.py
 * stats.py

In addition a new module was introduced: types
There all TypedDicts are included. Bitte geben Sie eine Commit-Beschreibung für Ihre Änderungen ein. Zeilen,
2022-04-17 08:55:38 -05:00
Sebastian Englbrecht
ebf4e43ced Modernizing Typing
All Dict, List were converted to dict, list, see: https://mypy.readthedocs.io/en/stable/builtin_types.html#generic-types
2022-04-17 08:55:38 -05:00
Blake Blackshear
a1afade9ba fix birdseye config 2022-04-16 09:08:10 -05:00
Blake Blackshear
b86b2d6602 cleanup print statements 2022-04-16 09:08:10 -05:00
Blake Blackshear
12abbc59d6 add lint check to workflow 2022-04-16 09:08:10 -05:00
Blake Blackshear
ec91466fe4 lint fixes 2022-04-16 09:08:10 -05:00
Blake Blackshear
0044f73d7a armv7 compatibility 2022-04-16 09:08:10 -05:00
Josh Hawkins
65e0ec7826 Allow improve_contrast to be toggled via mqtt (#3011)
* Toggle improve_contrast for cameras via MQTT

* Process parameter to mqtt toggle improve_contrast

* Update mqtt docs for improve_contrast topic

* Spacing

* Add class variable and update in process_frames

* Pass to constructor

* pass by reference mistake

* remove parameter

* remove parameter
2022-04-16 08:52:02 -05:00
Nicolas Mowen
a5016afdd4 FEAT: Ability to reorder & ability to hide Cameras in UI (#2981)
* Add options for reordering and hiding cameras selectively

* Add newline at end of camera file

* Make each camera for birdseye togglable as well

* Update names to be less ambiguous

* Update defaults

* Include sidebar change

* Remove birdseye toggle (will be added in separate PR)

* Remove birdseye toggle (will be added in separate PR)

* Remove birdseye toggle (will be added in separate PR)

* Update sidebar to only sort cameras once

* Simplify sorting logic
2022-04-15 07:23:02 -05:00
Nicolas Mowen
6808ba1b3b Add more issue templates (#3095)
* Create camera_support_request.yml

* Update camera_support_request.yml

* Create config_support_request.yml

* Update and rename support_request.yml to general_support_request.yml
2022-04-15 07:03:04 -05:00
Sebastian Englbrecht
41f58c7692 Add basic typing for multiple modules:
* log.py
 * video.py
 * watchdog.py
 * zeroconf.py
2022-04-15 07:01:43 -05:00
Sebastian Englbrecht
c6234bf548 fix depreciated import from collections 2022-04-15 07:01:43 -05:00
Sebastian Englbrecht
d995761419 Prepare mypy for typing checks 2022-04-15 07:01:43 -05:00
Nicolas Mowen
d749cf2e6b Allow birdseye to be overridden at the camera level (#3083)
* Add camera level processing for birdseye

* Add camera level birdseye configruation

* Propogate birdseye from global

* Update docs to show that birdseye is overridable

* Fix incorrect default factory

* Update note to indicate values that can be overridden

* Cleanup config accessing

* Add tests for birdseye config behavior

* Fix mistake on test format

* Update tests
2022-04-15 06:59:30 -05:00
herostrat
164e9b7eb8 Use requirement file for pip installs (#3090)
Co-authored-by: Sebastian Englbrecht <sebastian.englbrecht@kabelmail.de>
2022-04-12 07:21:21 -05:00
herostrat
d43f9189a6 Changes group type from int to str (#3086)
Co-authored-by: Sebastian Englbrecht <sebastian.englbrecht@kabelmail.de>
2022-04-12 07:20:28 -05:00
Nicolas Mowen
a292f272e9 DOCS: Add stationary tracking config section to elaborate more on stationary tracking (#3077)
* Add docs to elaborate more on stationary tracking

* Add link to guide on avoiding stationary objects in driveway scenario

* Update wording in reference config

* Small cleanups

* Update with PR comments
2022-04-11 07:19:25 -05:00
Nicolas Mowen
58c32857d3 Add latest frigate version to stats endpoints (#3038)
* Add latest version to stats in mqtt and http

* Update json to include new field

* Update to use requests

* Don't use incorrect exception
2022-04-11 07:10:19 -05:00
Blake Blackshear
35bd1de5ba limit send to plus where appropriate (#3080) 2022-04-11 06:56:53 -05:00
Nicolas Mowen
51fd18f56d FEAT: Ability to set custom birdseye icon and birdseye docs (#2979)
* Show custom.png for birdseye icon if available

* Don't look for config value

* Add birdseye docs
2022-04-10 09:15:56 -05:00
Nicolas Mowen
162e275ef3 Always show camera recordings in sidebar regardless of state (#2846) 2022-04-10 09:13:30 -05:00
Blake Blackshear
44a2b54773 package updates 2022-04-10 09:11:16 -05:00
Blake Blackshear
d17828931b include prettier in extensions 2022-04-10 09:11:16 -05:00
Blake Blackshear
cef77fba01 add frontend for frigate+ submission 2022-04-10 09:11:16 -05:00
Blake Blackshear
e724fe3da6 add endpoint to submit to plus 2022-04-10 09:11:16 -05:00
Nick
045aac8933 Add object filter ratio (#2952)
* Add object ratio config parameters

Issue: #2948

* Add config test for object filter ratios

Issue: #2948

* Address review comments

- Accept `ratio` default
- Rename `bounds` to `box` for consistency
- Add migration for new field

Issue: #2948

* Fix logical errors

- field migrations require default values
- `clipped` referenced the wrong index for region, since it shifted
- missed an inclusion of `ratio` for detections in `process_frames`
- revert naming `o[2]` as `box` since it is out of scope!

This has now been test-run against a video, so I believe the kinks are
worked out.

Issue: #2948

* Update contributing notes for `make`

Issue: #2948

* Fix migration

- Ensure that defaults match between Event and migration script
- Deconflict migration script number (from rebase)

Issue: #2948

* Filter objects out of ratio bounds

Issue: #2948

* Update migration file to 009

Issue: #2948
2022-04-10 08:25:18 -05:00
Nicolas Mowen
2e5d082ef3 Update docs to include warnings about needing record enabled in the config (#3045)
* Update record docs to include note for automations.

* Update config to warn about recording needing to be enabled.

* Update wording from PR comments
2022-04-02 08:33:10 -05:00
Nicolas Mowen
923d07b1a4 BUG: Event stuck if recording & snapshot disabled while in-progress (#3023)
* Fix recording getting stuck bug

* Fix typo
2022-03-30 06:33:34 -05:00
Dermot Duffy
c424c4b7ef Fix tiny timing bug. (#2994) 2022-03-21 06:43:27 -05:00
Nicolas Mowen
b1cc64d4fa FEAT: Ability to set sub labels for specific events (#2949)
* Add sub label to model and set / delete funs

* Add migrations for sub label

* Tweaks to API and model

* Show sublabel if available

* Cleanups

* Update docs

* Show person in UI title

* Fix typo and don't fail on no json

* Transfer sub labels for in progress events

* Remove sublabel reset

* Remove person only check

* Make default null

* Update docs and formatting

* Make default null

* Make nullable in migration

* Undo null

* Update model to accept null

* Update migration to accept null

* Don't set to default values

* Remove redundant defaults and update http logic

* Only need a single route

* Enforce 20 character limit in http

* Update docs to mention 20 character limit

* Cleanup

* Separate insert and update to make sure updated values are retained when event ends

* Use insert instead of replace

* Remove redundant if and have should_update_db include clip or snapshot requirement.
2022-03-17 07:18:43 -05:00
Nicolas Mowen
0abd0627df FEAT: Replace best jpg endpoint (#2944)
* Added object thumbnail def and made camera tracked objects use it.

* Add object snapshot def

* Remove documentation for best.jpg

* Update docs for label thumbnail and snapshot defs
2022-03-11 07:56:39 -06:00
Blake Blackshear
dccfc3b84f fix camera list on debug page 2022-03-11 07:49:06 -06:00
Blake Blackshear
deb3536cb2 fix date picker 2022-03-11 07:49:06 -06:00
Blake Blackshear
1d8f1b24a9 update to node 16 2022-03-11 07:49:06 -06:00
Blake Blackshear
7cabf8e5f5 run tests in container 2022-03-11 07:49:06 -06:00
Blake Blackshear
ffee9c8065 update makefile and docker dir 2022-03-11 07:49:06 -06:00
Blake Blackshear
00112eb7bc update frontend docs 2022-03-11 07:49:06 -06:00
Blake Blackshear
9bbe75d64e fix dark mode 2022-03-11 07:49:06 -06:00
Blake Blackshear
95fe62e141 switch to vite 2022-03-11 07:49:06 -06:00
Blake Blackshear
1c9ba11e07 swr events refactor 2022-03-11 07:49:06 -06:00
Blake Blackshear
4bae3993da update dev compose to assign groups 2022-03-11 07:49:06 -06:00
Blake Blackshear
d03865d1a5 add amd64 specific packages to container 2022-03-11 07:49:06 -06:00
707Alex707
83481afee1 remove print statement 2022-03-10 20:02:10 -06:00
Blake Blackshear
b1a2b0cda2 make dynamic contrast optional and disable by default 2022-03-10 19:46:55 -06:00
Blake Blackshear
0dfba6e8d9 bump version 2022-03-10 19:46:55 -06:00
Nicolas Mowen
e4afe50509 Docs: Update recording docs to include examples of retain modes. (#2914)
* Update recording docs to include examples of retain modes.

* Minor adjustment

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2022-03-10 06:37:20 -06:00
Nicolas Mowen
006569391f MQTT support for all objects for each camera & zone (#2908) 2022-03-10 06:03:00 -06:00
Blake Blackshear
f4c3bb0617 affiliate link updates 2022-03-01 18:45:56 -06:00
JohnMark Sill
3e07d4eddb feat: Timeline UI (#2830) 2022-02-27 08:04:12 -06:00
Nicolas Mowen
4004048add Ability to retain specific clips / events indefinitely (#2831) 2022-02-21 22:03:01 -06:00
Blake Blackshear
cbf26e09a4 fix for opencv changes 2022-02-21 06:51:33 -06:00
Blake Blackshear
20b4b503f0 refactor dockerbuild for multiarch 2022-02-21 06:51:33 -06:00
Blake Blackshear
08f573aaa5 Clarify max_frames setting 2022-02-20 08:17:43 -06:00
Blake Blackshear
3dd3786055 increment version 2022-02-18 22:15:30 -06:00
Blake Blackshear
bfecee9650 add missing optional comment in docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
395c16300d deregister based on max_frames setting 2022-02-18 21:18:26 -06:00
Blake Blackshear
ff19cdb773 refactor stationary config into section 2022-02-18 21:18:26 -06:00
Nicolas Mowen
5627b66a6e Always show recording link even if recordings are currently disabled (#2787)
* Always show recording link even if recordings are currently disabled

* Fix test to consider all cameras to have recording link
2022-02-18 21:18:26 -06:00
Blake Blackshear
ebdfbfe96c update birdseye to handle stationary objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
1a009c7fd1 use second stream in docs example 2022-02-18 21:18:26 -06:00
Blake Blackshear
c14f986fae stop forcing detection all the way to stationary_threshold 2022-02-18 21:18:26 -06:00
Blake Blackshear
ee5b9986ad bump default stationary_threshold to 10s 2022-02-18 21:18:26 -06:00
Blake Blackshear
190f217b13 set stationary_threshold default to 5x fps 2022-02-18 21:18:26 -06:00
Blake Blackshear
e78662b924 fix the bounding box calculation position at 10 2022-02-18 21:18:26 -06:00
Blake Blackshear
5a2e395352 selectively increment position changes 2022-02-18 21:18:26 -06:00
Jason Hunter
8de15af7b4 Fix duration for long events and playback rate for top of the hour 2022-02-18 21:18:26 -06:00
Jason Hunter
21178613de Only send significant update once when motionless count reaches the defined threshold. 2022-02-18 21:18:26 -06:00
Jason Hunter
340be7f86d Allow download of in progress clips 2022-02-18 21:18:26 -06:00
Blake Blackshear
0ff4acd59c remove invalid warning 2022-02-18 21:18:26 -06:00
Jason Hunter
28dd43f8ae Fix playback rate resetting to 1 on source change 2022-02-18 21:18:26 -06:00
Jason Hunter
56d24cbf6d Update package-lock.json 2022-02-18 21:18:26 -06:00
Jason Hunter
e433bec17f Add in progress events to recordings view 2022-02-18 21:18:26 -06:00
Blake Blackshear
3189467a36 update an object once per minute 2022-02-18 21:18:26 -06:00
Blake Blackshear
63536d249f signal an update when object becomes stationary 2022-02-18 21:18:26 -06:00
Blake Blackshear
3e90f3032c make stationary_threshold configurable 2022-02-18 21:18:26 -06:00
Blake Blackshear
5cff849e59 publish an update on position changes 2022-02-18 21:18:26 -06:00
Blake Blackshear
06cc7527a9 only update db entry when a stored property changes 2022-02-18 21:18:26 -06:00
Blake Blackshear
d78dc2388c increment motionless_count 2022-02-18 21:18:26 -06:00
Blake Blackshear
583912db9c allow motion based retention when detect is disabled 2022-02-18 21:18:26 -06:00
Blake Blackshear
5792cf042e fix resolution on reolink example 2022-02-18 21:18:26 -06:00
Blake Blackshear
4524dca3ed clarify addon versions 2022-02-18 21:18:26 -06:00
Blake Blackshear
304a569c7e remove outdated output args tip 2022-02-18 21:18:26 -06:00
Blake Blackshear
10c200dc24 clarify that zones are based on the bottom center 2022-02-18 21:18:26 -06:00
Blake Blackshear
0b8617d09f update addon urls 2022-02-18 21:18:26 -06:00
Blake Blackshear
e7026dfd6e add example for ios camera live feed notification 2022-02-18 21:18:26 -06:00
Blake Blackshear
b82d75b79e avoid rare divide by zero 2022-02-18 21:18:26 -06:00
Blake Blackshear
ac30091258 note for future 2022-02-18 21:18:26 -06:00
Blake Blackshear
de58bdcc9f improve warning for retain modes 2022-02-18 21:18:26 -06:00
Blake Blackshear
329e5f8f91 invert active_count logic 2022-02-18 21:18:26 -06:00
Blake Blackshear
4a16171f96 set has_clip to false when recordings fail 2022-02-18 21:18:26 -06:00
Blake Blackshear
f0212c2aa4 adjust error messages on ffmpeg crash 2022-02-18 21:18:26 -06:00
Blake Blackshear
7401cf2399 add stacktrace to config validation errors 2022-02-18 21:18:26 -06:00
Blake Blackshear
493d16519a add new properties to the docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
a87675d3cc add additional info for non-H264 cameras 2022-02-18 21:18:26 -06:00
Blake Blackshear
0bef16bb17 upgrade npm in dev container 2022-02-18 21:18:26 -06:00
Blake Blackshear
e0078f388e package updates for docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
cf6e66c453 allow dash in camera name 2022-02-18 21:18:26 -06:00
Blake Blackshear
34fa53afcc make motion the default retain mode 2022-02-18 21:18:26 -06:00
Blake Blackshear
663bf05fd7 update stationary interval docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
f512af2563 make expire interval configurable for users wanting to minimize i/o 2022-02-18 21:18:26 -06:00
Blake Blackshear
7e7d70aa5b avoid extra tracking work on stationary frames 2022-02-18 21:18:26 -06:00
Blake Blackshear
dd1cf4d2ce use iou instead of centroid 2022-02-18 21:18:26 -06:00
Blake Blackshear
e627f4e935 dont stop scanning when there are other regions 2022-02-18 21:18:26 -06:00
Blake Blackshear
c6445898ce default periodic checks to never 2022-02-18 21:18:26 -06:00
Blake Blackshear
f1ddd0e6f7 scan the frame on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
db369a5b7f require a position change to be an active object 2022-02-18 21:18:26 -06:00
Blake Blackshear
87cd618998 randomize the region multiplier for variation 2022-02-18 21:18:26 -06:00
Blake Blackshear
338e4004d4 improve method for determining position
compares the centroid to a history of bounding boxes
2022-02-18 21:18:26 -06:00
Blake Blackshear
675f21e23a if recording not on disk, delete from db and return 2022-02-18 21:18:26 -06:00
Blake Blackshear
4d2d11193f cleanup clean snapshots on event deletion too 2022-02-18 21:18:26 -06:00
Blake Blackshear
69aaf1f8e6 require url safe camera names 2022-02-18 21:18:26 -06:00
Bernt Christian Egeland
a10970d7c9 Event Datepicker (#2428)
* new datepicker

* dev

* dev

* dev

* fix for version 0.10

* added rounded corners for date range

* lint

* Commented out some Select.test.

* improved date range selection

* improved functions with useCallback

* improved Select.test.jsx

* keyboard navigation

* keyboard navigation

* added dropdown menu icon

* Hide filters on xs, Button to show

* check if to far left before right

* Filter button text

* improved local timezone
2022-02-18 21:18:26 -06:00
Yuriy Sannikov
6eecb6780e Run python unit tests in a github actions (#2589)
* tox tests initial commit

* run tests in the Dockerfile during the build phase

* remove local tests

Co-authored-by: YS <ys@gm.com>
2022-02-18 21:18:26 -06:00
Yuriy Sannikov
80627e4989 safe refactoring (#2552)
Co-authored-by: YS <ys@gm.com>
2022-02-18 21:18:26 -06:00
TJ Horner
9e987fdebc Change JPEG mime type (#2543) 2022-02-18 21:18:26 -06:00
Blake Blackshear
e6292c719d disable disk sync on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
7c74bf2566 fix migrations 2022-02-18 21:18:26 -06:00
Blake Blackshear
2c91e7853c check for apex dir 2022-02-18 21:18:26 -06:00
Ryan McLean
1e7f196e5c #2117 change entered_zones from set to list so that they are not automatically alphabetically ordered (#2212) 2022-02-18 21:18:26 -06:00
Justin Goette
f91f4f0053 Allow for ".yaml" (#2244)
* allow for ".yaml"

* remove unused import
2022-02-18 21:18:26 -06:00
Matt Clayton
8b2622a234 Add temperature of coral tpu to telemetry mqtt message 2022-02-18 21:18:26 -06:00
Blake Blackshear
a2ddb12eb3 limit vod response cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
985bd6d9bd update docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
ec3c15e4a7 expire overlapping segments based on mode 2022-02-18 21:18:26 -06:00
Blake Blackshear
188b202836 store objects and motion counts in the db 2022-02-18 21:18:26 -06:00
Blake Blackshear
01e607a14e warn when retention mismatch 2022-02-18 21:18:26 -06:00
Blake Blackshear
5b164b72dc refactor segment stats logic 2022-02-18 21:18:26 -06:00
Blake Blackshear
dcf65febba switch to retain config instead of retain_days 2022-02-18 21:18:26 -06:00
Blake Blackshear
56a2d4e64d pass processed tracked objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
ef214fb80a retain frame data for recording maintenance 2022-02-18 21:18:26 -06:00
Blake Blackshear
93f418ac0b fix process_clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
689af4ff87 sync recordings with disk once on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
4ab0927de8 no need to expire recordings every minute 2022-02-18 21:18:26 -06:00
Blake Blackshear
014e6fc909 ensure cache copies when events have ended 2022-02-18 21:18:26 -06:00
Blake Blackshear
6832575643 cleanup missing files from database once per hour 2022-02-18 21:18:26 -06:00
Blake Blackshear
07ad2d97b1 handle missing file edge case 2022-02-18 21:18:26 -06:00
Blake Blackshear
039f1a522e log error messages on vod endpoints 2022-02-18 21:18:26 -06:00
Blake Blackshear
24e2f84231 ensure duration > 0 for segments 2022-02-18 21:18:26 -06:00
Blake Blackshear
e0c0033852 use snapshot url to support in progress events 2022-02-18 21:18:26 -06:00
Blake Blackshear
c50e9d48bf ensure stationary interval is greater than 0 2022-02-18 21:18:26 -06:00
Blake Blackshear
173eaabddf add duration to cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
a748b70da1 avoid running ffprobe for each segment multiple times 2022-02-18 21:18:26 -06:00
Blake Blackshear
8eabe5dd41 warn if no wait time 2022-02-18 21:18:26 -06:00
Blake Blackshear
114415b5e1 keep 5 segments in cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
ba55b5a6db better cache handling 2022-02-18 21:18:26 -06:00
Blake Blackshear
7533f2a8ab avoid proactive messages with retain_days 0 and handle first pass 2022-02-18 21:18:26 -06:00
Blake Blackshear
543a8a1712 avoid divide by zero 2022-02-18 21:18:26 -06:00
Blake Blackshear
9b23ff597c revert switch to b/w frame prep 2022-02-18 21:18:26 -06:00
Blake Blackshear
b2ce1edd5a fix default motion comment 2022-02-18 21:18:26 -06:00
Blake Blackshear
a0235b7da4 more robust cache management 2022-02-18 21:18:26 -06:00
Blake Blackshear
87e2300855 set retain when setting switches from frontend 2022-02-18 21:18:26 -06:00
Blake Blackshear
34bc6a6457 error handling for the recording maintainer 2022-02-18 21:18:26 -06:00
Blake Blackshear
273076e7f4 don't modify ffmpeg_cmd object 2022-02-18 21:18:26 -06:00
Blake Blackshear
b29b311e92 fix ffmpeg config for env vars 2022-02-18 21:18:26 -06:00
Blake Blackshear
5a9e82c4b0 create ffmpeg commands on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
6218791708 clarify shm in docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
0e43f452d2 use resolution of clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
0695bb097d revamp process clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
294c79a271 no longer make motion settings dynamic 2022-02-18 21:18:26 -06:00
Blake Blackshear
e351e132f5 remove min frame height of 180 and increase contour area 2022-02-18 21:18:26 -06:00
Blake Blackshear
258215a3ae consolidate regions 2022-02-18 21:18:26 -06:00
Blake Blackshear
08ddfc100f improve contrast 2022-02-18 21:18:26 -06:00
Blake Blackshear
8ab6cba521 check for overlapping motion boxes 2022-02-18 21:18:26 -06:00
Blake Blackshear
eb16de7395 config option for stationary detection interval 2022-02-18 21:18:26 -06:00
Blake Blackshear
dde0498ed3 drop high overlap detections 2022-02-18 21:18:26 -06:00
Blake Blackshear
75c8570913 reduce detection rate for stationary objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
e36099a342 improve box merging and keep tracking 2022-02-18 21:18:26 -06:00
Blake Blackshear
2f2329ba44 only save recordings when an event is in progress 2022-02-18 21:18:26 -06:00
Blake Blackshear
6c8b184d2c version tick 2022-02-18 21:18:26 -06:00
DataBitz
32878bd016 Another missing slash (#2803)
2nd attempt to fix link to full configuration
2022-02-14 07:33:29 -06:00
DataBitz
12d13988c4 Missing slash in url (#2797)
Missing slash in url to full-configuration-reference
2022-02-13 07:38:24 -06:00
atinsley
9e4d921488 Update advanced.md (#2794)
Add details about how to specify a custom database location in config.yml
2022-02-12 06:28:56 -06:00
Blake Blackshear
edc1884c4e add warning to storage docs 2022-02-11 06:15:15 -06:00
Alex Yao
a2d1bd2c67 Document JPEG streams (#2586)
* Document JPEG streams

* Update camera_specific.md
2022-02-02 07:27:22 -06:00
Felipe Santos
bb68a2405b Improve audio conversion tip (#2140)
* Improve audio convert guide

* Mention faq in RTMP configuration

* Add example for audio conversion tip

* Change comma to period

* Explain why this is needed
2021-12-29 08:57:32 -06:00
MrNorm
42ac4172ff Add passthrough information for PCIe Coral TPU (#2200) 2021-12-12 09:31:52 -06:00
hcooper
998921ae63 Update objects.mdx
Mention that `person` is the only tracked object by default. Minor reformat.
2021-12-01 07:33:16 -06:00
Blake Blackshear
26ae6084ea fix rtmp again 2021-10-24 13:53:43 -05:00
Blake Blackshear
76142e9699 version tick 2021-10-24 13:53:43 -05:00
Blake Blackshear
5e692acfbb add links in docs to other sites 2021-10-23 09:41:32 -05:00
Blake Blackshear
a67b8ab84d validate with runtime config (fixes #2055) 2021-10-23 08:21:15 -05:00
Blake Blackshear
4cf55ad8e2 Revert switch to mpegts format and audio default 2021-10-23 08:21:15 -05:00
Blake Blackshear
c1132e6897 update ignore files 2021-10-23 08:21:15 -05:00
Blake Blackshear
d6104f2eb2 add storage info to docs 2021-10-23 08:21:15 -05:00
Blake Blackshear
b0e0abe385 improve performance of cache loop 2021-10-23 08:21:15 -05:00
Blake Blackshear
4916e1cd1d hide banner for ffmpeg conversion 2021-10-23 08:21:15 -05:00
Blake Blackshear
cd87f3e6f4 fix old style recording cleanup 2021-10-23 08:21:15 -05:00
Blake Blackshear
18f4ab2644 version tick 2021-10-23 08:21:15 -05:00
Lindsay Ward
0bd3be94ec Clarify environment variables
Based on issue #1976 - specify explicitly that these fields can include environment variables to avoid interpretation that environment variables could be used anywhere.
I am participating in #hacktoberfest, so I would appreciate if you could add the 'hacktoberfest-accepted' label (or add #hacktoberfest topic to your repo). Thanks!
2021-10-23 06:42:53 -05:00
Blake Blackshear
25bb515afc Merge pull request #2026 from blakeblackshear/recording_fix
0.9.2
2021-10-19 20:43:25 -05:00
Blake Blackshear
7ab6961ee1 use live dimensions 2021-10-17 08:48:59 -05:00
Blake Blackshear
ae24cf3bb2 set max width/height for live view 2021-10-17 07:48:56 -05:00
Blake Blackshear
2e494477a6 backwards compatibility for segment_type 2021-10-16 10:36:13 -05:00
Blake Blackshear
80b72c75d9 revert jest update 2021-10-16 08:12:22 -05:00
Blake Blackshear
9494bb7f5f frontend dependency updates 2021-10-16 07:57:59 -05:00
Blake Blackshear
86a741b6e6 assign roles when single input and consolidate validation 2021-10-16 07:46:39 -05:00
Blake Blackshear
f738275d21 yell about config validation errors
for the people in the back
2021-10-16 07:17:36 -05:00
Blake Blackshear
e297e02800 store audio by default 2021-10-16 06:06:49 -05:00
Blake Blackshear
b2e05afff2 prevent oldest recording from being deleted 2021-10-15 21:56:03 -05:00
Blake Blackshear
05fc35fc3d update hardware docs 2021-10-15 21:29:36 -05:00
Blake Blackshear
c809494c98 switch to mpegts format for cache and create mp4 with faststart 2021-10-15 21:08:43 -05:00
Blake Blackshear
ef82c5c691 fix expiration when event spans the exire date 2021-10-15 07:30:55 -05:00
Blake Blackshear
c0e2a75715 version tick 2021-10-15 07:30:35 -05:00
Blake Blackshear
01ddd00bc5 Merge pull request #1975 from blakeblackshear/hassos_docs
update hassos warning
2021-10-10 07:39:11 -05:00
Blake Blackshear
d150f01a2c update hassos warning 2021-10-10 07:32:55 -05:00
Blake Blackshear
f9e159deaf Merge pull request #1968 from FM-17/patch-1
warning for dev board incompatibility post-0.9.x
2021-10-09 11:57:46 -05:00
FM-17
381b00157e warning for dev board incompatibility post-0.9.x
Hoped to investigate this with my dev board at some point. In the meantime, added a warning for others who may experience it when upgrading to the new stable release.
2021-10-09 11:23:51 -03:00
Blake Blackshear
800f33e7be version tick 2021-10-05 19:02:38 -05:00
Blake Blackshear
b8218876be Merge pull request #1922 from blakeblackshear/fix_logo
fix logo used for birdseye
2021-10-05 18:57:07 -05:00
Blake Blackshear
5669f4c161 fix logo used for birdseye 2021-10-05 18:40:46 -05:00
Blake Blackshear
c492b30adb Merge pull request #825 from blakeblackshear/release-0.9.0
Release 0.9.0
2021-10-05 17:59:25 -05:00
Kevin Pelzel
eb48722126 added white background to apple-touch-icon 2021-10-05 17:37:18 -05:00
Blake Blackshear
8e881b60f0 update hardware recommendations 2021-10-05 07:13:13 -05:00
Blake Blackshear
0260d824a6 further doc clarifications 2021-10-05 06:57:17 -05:00
Blake Blackshear
0877a7dec7 Create config.yml 2021-10-04 17:20:58 -05:00
Blake Blackshear
4c7919ad69 updated links 2021-10-04 08:54:35 -05:00
Blake Blackshear
4e997124b3 update latest recommendations for reolink 2021-10-04 07:18:53 -05:00
Blake Blackshear
8b040f5c95 optimize images for web 2021-10-04 07:00:30 -05:00
Blake Blackshear
96156805ed Delete bug_report.md 2021-10-03 08:53:19 -05:00
Blake Blackshear
b8d48d7e62 Create support_request.yml 2021-10-03 08:51:53 -05:00
Blake Blackshear
8ca12806ca revert rockchip support for aarch64 2021-10-03 07:43:55 -05:00
Blake Blackshear
de811b7018 delete clean snapshot when duplicate 2021-10-02 06:59:02 -05:00
Blake Blackshear
7bf7365f6c better log message when corrupt segment detected 2021-10-02 06:58:29 -05:00
Blake Blackshear
1daffd92fd docs updates 2021-10-01 07:37:47 -05:00
Blake Blackshear
74986982a0 update docs url 2021-09-26 16:43:26 -05:00
254 changed files with 39435 additions and 12965 deletions

View File

@@ -9,19 +9,32 @@
"mhutchie.git-graph",
"ms-azuretools.vscode-docker",
"streetsidesoftware.code-spell-checker",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
"dbaeumer.vscode-eslint",
"mikestead.dotenv",
"csstools.postcss",
"blanu.vscode-styled-jsx",
"bradlc.vscode-tailwindcss"
],
"settings": {
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.languageServer": "Pylance",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash"
"eslint.workingDirectories": ["./web"],
"[json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsx][js][tsx][ts]": {
"editor.codeActionsOnSave": ["source.addMissingImports", "source.fixAll"],
"editor.tabSize": 2
},
"cSpell.ignoreWords": ["rtmp"],
"cSpell.words": ["preact"]
}
}

View File

@@ -7,4 +7,6 @@ config/
.git
core
*.mp4
*.db
*.jpg
*.db
*.ts

View File

@@ -1,56 +0,0 @@
---
name: Bug report or Support request
about: Bug report or Support request
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what your issue is.
**Version of frigate**
Output from `/api/version`
**Config file**
Include your full config file wrapped in triple back ticks.
```yaml
config here
```
**Frigate container logs**
```
Include relevant log output here
```
**Frigate stats**
```json
Output from frigate's /api/stats endpoint
```
**FFprobe from your camera**
Run the following command and paste output below
```
ffprobe <stream_url>
```
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Computer Hardware**
- OS: [e.g. Ubuntu, Windows]
- Install method: [e.g. Addon, Docker Compose, Docker Command]
- Virtualization: [e.g. Proxmox, Virtualbox]
- Coral Version: [e.g. USB, PCIe, None]
- Network Setup: [e.g. Wired, WiFi]
**Camera Info:**
- Manufacturer: [e.g. Dahua]
- Model: [e.g. IPC-HDW5231R-ZE]
- Resolution: [e.g. 720p]
- FPS: [e.g. 5]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,107 @@
name: Camera Support Request
description: Support for setting up cameras in Frigate
title: "[Camera Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: ffprobe
attributes:
label: FFprobe output from your camera
description: Run `ffprobe <camera_url>` and provide output below
render: shell
validations:
required: true
- type: textarea
id: stats
attributes:
label: Frigate stats
description: Output from frigate's /api/stats endpoint
render: json
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network connection
options:
- Wired
- Wireless
- Mixed
validations:
required: true
- type: input
id: camera
attributes:
label: Camera make and model
description: Dahua, hikvision, amcrest, reolink, etc and model number
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,82 @@
name: Config Support Request
description: Support for Frigate configuration
title: "[Config Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: stats
attributes:
label: Frigate stats
description: Output from frigate's /api/stats endpoint
render: json
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

View File

@@ -0,0 +1,107 @@
name: General Support Request
description: General support request for Frigate
title: "[Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: ffprobe
attributes:
label: FFprobe output from your camera
description: Run `ffprobe <camera_url>` and provide output below
render: shell
validations:
required: true
- type: textarea
id: stats
attributes:
label: Frigate stats
description: Output from frigate's /api/stats endpoint
render: json
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network connection
options:
- Wired
- Wireless
- Mixed
validations:
required: true
- type: input
id: camera
attributes:
label: Camera make and model
description: Dahua, hikvision, amcrest, reolink, etc and model number
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

View File

@@ -2,6 +2,9 @@ name: On pull request
on: pull_request
env:
DEFAULT_PYTHON: 3.9
jobs:
web_lint:
name: Web - Lint
@@ -10,25 +13,11 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
node-version: 16.x
- run: npm install
working-directory: ./web
- name: Lint
run: npm run lint:cmd
working-directory: ./web
web_build:
name: Web - Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: npm install
working-directory: ./web
- name: Build
run: npm run build
run: npm run lint
working-directory: ./web
web_test:
@@ -38,9 +27,54 @@ jobs:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
node-version: 16.x
- run: npm install
working-directory: ./web
- name: Test
run: npm run test
working-directory: ./web
python_checks:
runs-on: ubuntu-latest
name: Python checks
steps:
- name: Check out the repository
uses: actions/checkout@v2.3.4
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Install requirements
run: |
pip install pip
pip install -r requirements-dev.txt
- name: Lint
run: |
python3 -m black frigate --check
python_tests:
runs-on: ubuntu-latest
name: Python Tests
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/setup-node@master
with:
node-version: 16.x
- run: npm install
working-directory: ./web
- name: Build web
run: npm run build
working-directory: ./web
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Create Version Module
run: make version
- name: Build
run: make
- name: Run mypy
run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
- name: Run tests
run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest

View File

@@ -1,28 +0,0 @@
name: On push
on:
push:
branches:
- master
- release-0.8.0
jobs:
deploy-docs:
name: Deploy docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12.x
- run: npm install
- name: Build docs
run: npm run build
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build

3
.gitignore vendored
View File

@@ -6,9 +6,12 @@ debug
config/config.yml
models
*.mp4
*.ts
*.db
*.csv
frigate/version.py
web/build
web/node_modules
web/coverage
core
!/web/**/*.ts

View File

@@ -1,62 +1,39 @@
default_target: amd64_frigate
default_target: local
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
VERSION = 0.11.0
CURRENT_UID := $(shell id -u)
CURRENT_GID := $(shell id -g)
version:
echo "VERSION='0.9.0-$(COMMIT_HASH)'" > frigate/version.py
echo "VERSION=\"$(VERSION)-$(COMMIT_HASH)\"" > frigate/version.py
web:
docker build --tag frigate-web --file docker/Dockerfile.web web/
amd64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64 --file docker/Dockerfile.wheels .
amd64_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
build_web:
docker run --volume ${PWD}/web:/web -w /web --volume /etc/passwd:/etc/passwd:ro --volume /etc/group:/etc/group:ro -u $(CURRENT_UID):$(CURRENT_GID) node:16 /bin/bash -c "npm install && npm run build"
nginx_frigate:
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate-nginx:1.0.2 --file docker/Dockerfile.nginx .
amd64_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.amd64 .
local:
DOCKER_BUILDKIT=1 docker build -t frigate -f docker/Dockerfile .
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
amd64:
docker buildx build --platform linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64nvidia --file docker/Dockerfile.wheels .
arm64:
docker buildx build --platform linux/arm64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
armv7:
docker buildx build --platform linux/arm/v7 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.amd64nvidia .
build: version amd64 arm64 armv7
docker buildx build --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
push: build
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
aarch64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels .
run_tests: frigate
docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
aarch64_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.3.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.3.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.aarch64 .
aarch64_all: aarch64_wheels aarch64_ffmpeg aarch64_frigate
armv7_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
armv7_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
armv7_frigate: version web
docker build --no-cache --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
docker build --no-cache --tag frigate --file docker/Dockerfile.armv7 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
.PHONY: web
.PHONY: run_tests

View File

@@ -20,7 +20,7 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
## Documentation
View the documentation at https://blakeblackshear.github.io/frigate
View the documentation at https://docs.frigate.video
## Donations

View File

@@ -3,20 +3,28 @@ services:
dev:
container_name: frigate-dev
user: vscode
privileged: true
# add groups from host for render, plugdev, video
group_add:
- "109" # render
- "110" # render
- "44" # video
- "46" # plugdev
shm_size: "256mb"
build:
context: .
dockerfile: docker/Dockerfile.dev
devices:
- /dev/bus/usb:/dev/bus/usb
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
volumes:
- /etc/localtime:/etc/localtime:ro
- .:/lab/frigate:cached
- ./config/config.yml:/config/config.yml:ro
- ./debug:/media/frigate
- /dev/bus/usb:/dev/bus/usb
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
ports:
- "1935:1935"
- "3000:3000"
- "5000:5000"
- "5001:5001"
- "8080:8080"

138
docker/Dockerfile Normal file
View File

@@ -0,0 +1,138 @@
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
FROM debian:11 as wheels
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive
# Use a separate container to build wheels to prevent build dependencies in final image
RUN apt-get -qq update \
&& apt-get -qq install -y \
apt-transport-https \
gnupg \
wget \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-get -qq update \
&& apt-get -qq install -y \
python3 \
python3-dev \
wget \
# opencv dependencies
build-essential cmake git pkg-config libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
# scipy dependencies
gcc gfortran libopenblas-dev liblapack-dev
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python3 get-pip.py "pip"
RUN if [ "${TARGETARCH}" = "arm" ]; \
then echo "[global]" > /etc/pip.conf \
&& echo "extra-index-url=https://www.piwheels.org/simple" >> /etc/pip.conf; \
fi
COPY requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt
COPY requirements-wheels.txt /requirements-wheels.txt
RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
# Frigate Container
FROM debian:11-slim
ARG TARGETARCH
ARG JELLYFIN_FFMPEG_VERSION=5.0.1-7
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
ENV FLASK_ENV=development
COPY --from=wheels /wheels /wheels
# Install ffmpeg
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
apt-transport-https \
gnupg \
wget \
unzip tzdata libxml2 xz-utils \
python3-pip \
# add raspberry pi repo
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
# add coral repo
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
# enable non-free repo
&& sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list \
&& apt-get -qq update \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
# coral drivers
libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& pip3 install -U /wheels/*.whl \
# jellyfin-ffmpeg
&& wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg5_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y ./jellyfin.deb \
&& rm jellyfin.deb \
# arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers intel-media-va-driver-non-free; \
fi \
# not sure why 32bit arm requires all these
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev; \
fi \
&& rm -rf /wheels \
&& apt-get remove gnupg apt-transport-https -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
ENV PATH=$PATH:/usr/lib/jellyfin-ffmpeg
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels
COPY labelmap.txt /labelmap.txt
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
WORKDIR /opt/frigate/
ADD frigate frigate/
ADD migrations migrations/
COPY web/dist web/
COPY docker/rootfs/ /
# s6-overlay
RUN S6_ARCH="${TARGETARCH}" \
&& if [ "${TARGETARCH}" = "amd64" ]; then S6_ARCH="amd64"; fi \
&& if [ "${TARGETARCH}" = "arm" ]; then S6_ARCH="armhf"; fi \
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
&& wget -O /tmp/s6-overlay-installer "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-${S6_ARCH}-installer" \
&& chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
EXPOSE 5000
EXPOSE 1935
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@@ -1,28 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg runtime dependencies
libgomp1 \
# runtime dependencies
libopenexr24 \
libgstreamer1.0-0 \
libgstreamer-plugins-base1.0-0 \
libopenblas-base \
libjpeg-turbo8 \
libpng16-16 \
libtiff5 \
libdc1394-22 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-aarch64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-aarch64-installer && /tmp/s6-overlay-aarch64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@@ -1,28 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
# By default, use the i965 driver
ENV LIBVA_DRIVER_NAME=i965
# Install packages for apt repo
RUN wget -qO - https://repositories.intel.com/graphics/intel-graphics.key | apt-key add - \
&& echo 'deb [arch=amd64] https://repositories.intel.com/graphics/ubuntu focal main' > /etc/apt/sources.list.d/intel-graphics.list \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F63F0F2B90935439 \
&& echo 'deb http://ppa.launchpad.net/kisak/kisak-mesa/ubuntu focal main' > /etc/apt/sources.list.d/kisak-mesa-focal.list
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg dependencies
libgomp1 \
# VAAPI drivers for Intel hardware accel
libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver-non-free mesa-vdpau-drivers mesa-va-drivers mesa-vdpau-drivers libdrm-radeon1 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@@ -1,51 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg dependencies
libgomp1 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# nvidia layer (see https://gitlab.com/nvidia/container-images/cuda/blob/master/dist/11.1/ubuntu20.04-x86_64/base/Dockerfile)
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
RUN apt-get update && apt-get install -y --no-install-recommends \
gnupg2 curl ca-certificates && \
curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/7fa2af80.pub | apt-key add - && \
echo "deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/cuda.list && \
echo "deb https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu2004/x86_64 /" > /etc/apt/sources.list.d/nvidia-ml.list && \
apt-get purge --autoremove -y curl \
&& rm -rf /var/lib/apt/lists/*
ENV CUDA_VERSION 11.1.1
# For libraries in the cuda-compat-* package: https://docs.nvidia.com/cuda/eula/index.html#attachment-a
RUN apt-get update && apt-get install -y --no-install-recommends \
cuda-cudart-11-1=11.1.74-1 \
cuda-compat-11-1 \
&& ln -s cuda-11.1 /usr/local/cuda && \
rm -rf /var/lib/apt/lists/*
# Required for nvidia-docker v1
RUN echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf && \
echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf
ENV PATH /usr/local/nvidia/bin:/usr/local/cuda/bin:${PATH}
ENV LD_LIBRARY_PATH /usr/local/nvidia/lib:/usr/local/nvidia/lib64
# nvidia-container-runtime
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV NVIDIA_REQUIRE_CUDA "cuda>=11.1 brand=tesla,driver>=418,driver<419 brand=tesla,driver>=440,driver<441 brand=tesla,driver>=450,driver<451"
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@@ -1,30 +0,0 @@
FROM frigate-base
LABEL maintainer "blakeb@blakeshome.com"
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
# ffmpeg runtime dependencies
libgomp1 \
# runtime dependencies
libopenexr24 \
libgstreamer1.0-0 \
libgstreamer-plugins-base1.0-0 \
libopenblas-base \
libjpeg-turbo8 \
libpng16-16 \
libtiff5 \
libdc1394-22 \
libaom0 \
libx265-179 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
# s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-armhf-installer /tmp/
RUN chmod +x /tmp/s6-overlay-armhf-installer && /tmp/s6-overlay-armhf-installer /
ENTRYPOINT ["/init"]
CMD ["python3", "-u", "-m", "frigate"]

View File

@@ -1,55 +0,0 @@
ARG ARCH=amd64
ARG WHEELS_VERSION
ARG FFMPEG_VERSION
ARG NGINX_VERSION
FROM blakeblackshear/frigate-wheels:${WHEELS_VERSION}-${ARCH} as wheels
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
FROM blakeblackshear/frigate-nginx:${NGINX_VERSION} as nginx
FROM frigate-web as web
FROM ubuntu:20.04
LABEL maintainer "blakeb@blakeshome.com"
COPY --from=ffmpeg /usr/local /usr/local/
COPY --from=wheels /wheels/. /wheels/
ENV FLASK_ENV=development
# ENV FONTCONFIG_PATH=/etc/fonts
ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get upgrade -y \
&& apt-get -qq install --no-install-recommends -y gnupg wget unzip tzdata libxml2 \
&& apt-get -qq install --no-install-recommends -y python3-pip \
&& pip3 install -U /wheels/*.whl \
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
&& apt-get -qq update && apt-get -qq install --no-install-recommends -y libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& rm -rf /var/lib/apt/lists/* /wheels \
&& (apt-get autoremove -y; apt-get autoclean -y)
RUN pip3 install \
peewee_migrate \
pydantic \
zeroconf \
ws4py
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels
COPY labelmap.txt /labelmap.txt
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
WORKDIR /opt/frigate/
ADD frigate frigate/
ADD migrations migrations/
COPY --from=web /opt/frigate/build web/
COPY docker/rootfs/ /
EXPOSE 5000
EXPOSE 1935

View File

@@ -6,7 +6,7 @@ ARG USER_GID=$USER_UID
# Create the user
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \
#
# [Optional] Add sudo support. Omit if you don't need to install software after connecting.
&& apt-get update \
@@ -17,8 +17,11 @@ RUN groupadd --gid $USER_GID $USERNAME \
RUN apt-get update \
&& apt-get install -y git curl vim htop
RUN pip3 install pylint black
COPY requirements-dev.txt /opt/frigate/requirements-dev.txt
RUN pip3 install -r requirements-dev.txt
# Install Node 14
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
# Install Node 16
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
&& apt-get install -y nodejs
RUN npm install -g npm@latest

View File

@@ -1,486 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 xutils-dev && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.11.0 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="a45c6b403413abd5706f3582f04c8339d26397c4304b78fa552f2215df64101f freetype-2.11.0.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
linux-headers-raspi2 \
libomxil-bellagio-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
export CXXFLAGS="${CXXFLAGS} -fPIC" && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make -j $(nproc) install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make check && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/rkmpp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone https://github.com/rockchip-linux/libdrm-rockchip && git clone https://github.com/rockchip-linux/mpp && \
cd libdrm-rockchip && bash autogen.sh && ./configure && make && make install && \
cd ../mpp && cmake -DRKPLATFORM=ON -DHAVE_DRM=ON && make -j6 && make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-rkmpp \
--enable-libdrm \
# --enable-omx \
# --enable-omx-rpi \
# --enable-mmal \
--enable-v4l2_m2m \
--enable-neon \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make -j $(nproc) && \
make -j $(nproc) install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/
# Run ffmpeg with -c:v h264_v4l2m2m to enable HW accell for decoding on raspberry pi4 64-bit

View File

@@ -1,468 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
libva-dev \
libmfx-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make && \
make install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make && \
make install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make && \
make install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make && \
make install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make && \
make install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make check && \
make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmfx \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-vaapi \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make && \
make install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/
RUN \
apt-get update -y && \
apt-get install -y --no-install-recommends libva-drm2 libva2 i965-va-driver mesa-va-drivers && \
rm -rf /var/lib/apt/lists/*

View File

@@ -1,549 +0,0 @@
# inspired by https://github.com/jrottenberg/ffmpeg/blob/master/docker-images/4.3/ubuntu1804/Dockerfile
# ffmpeg - http://ffmpeg.org/download.html
#
# From https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu
#
# https://hub.docker.com/r/jrottenberg/ffmpeg/
#
#
FROM nvidia/cuda:11.1-devel-ubuntu20.04 AS devel-base
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /tmp/workdir
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM nvidia/cuda:11.1-runtime-ubuntu20.04 AS runtime-base
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /tmp/workdir
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 libxcb-shape0-dev && \
apt-get autoremove -y && \
apt-get clean -y
FROM devel-base as build
ENV NVIDIA_HEADERS_VERSION=9.1.23.1
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.2 \
LIBSRT_VERSION=1.4.1 \
LIBARIBB24_VERSION=1.0.3 \
LIBPNG_VERSION=1.6.9 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LIBZMQ_SHA256SUM="02ecc88466ae38cf2c8d79f09cfd2675ba299a439680b64ade733e26a349edeb v4.3.2.tar.gz"
ARG LIBARIBB24_SHA256SUM="f61560738926e57f9173510389634d8c06cabedfa857db4b28fb7704707ff128 v1.0.3.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
libssl-dev \
yasm \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
RUN \
DIR=/tmp/nv-codec-headers && \
git clone https://github.com/FFmpeg/nv-codec-headers ${DIR} && \
cd ${DIR} && \
git checkout n${NVIDIA_HEADERS_VERSION} && \
make PREFIX="${PREFIX}" && \
make install PREFIX="${PREFIX}" && \
rm -rf ${DIR}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make && \
make install && \
rm -rf ${DIR}
### x265 http://x265.org/
RUN \
DIR=/tmp/x265 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
tar -zx && \
cd x265_${X265_VERSION}/build/linux && \
sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
./multilib.sh && \
make -C 8bit install && \
rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make && \
make install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make && \
make install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make && \
make install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make && \
make install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/aom && \
git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
cd ${DIR} ; \
rm -rf CMakeCache.txt CMakeFiles ; \
mkdir -p ./aom_build ; \
cd ./aom_build ; \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
make ; \
make install ; \
rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make && \
make install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
echo ${LIBZMQ_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make && \
make check && \
make install && \
rm -rf ${DIR}
## libsrt https://github.com/Haivision/srt
RUN \
DIR=/tmp/srt && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/Haivision/srt/archive/v${LIBSRT_VERSION}.tar.gz && \
tar -xz --strip-components=1 -f v${LIBSRT_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make && \
make install && \
rm -rf ${DIR}
## libpng
RUN \
DIR=/tmp/png && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone https://git.code.sf.net/p/libpng/code ${DIR} -b v${LIBPNG_VERSION} --depth 1 && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make check && \
make install && \
rm -rf ${DIR}
## libaribb24
RUN \
DIR=/tmp/b24 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/nkoriyama/aribb24/archive/v${LIBARIBB24_VERSION}.tar.gz && \
echo ${LIBARIBB24_SHA256SUM} | sha256sum --check && \
tar -xz --strip-components=1 -f v${LIBARIBB24_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure CFLAGS="-I${PREFIX}/include -fPIC" --prefix="${PREFIX}" && \
make && \
make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-libsrt \
--enable-libaribb24 \
--enable-nvenc \
--enable-cuda \
--enable-cuvid \
--enable-libnpp \
--extra-cflags="-I${PREFIX}/include -I${PREFIX}/include/ffnvcodec -I/usr/local/cuda/include/" \
--extra-ldflags="-L${PREFIX}/lib -L/usr/local/cuda/lib64 -L/usr/local/cuda/lib32/" && \
make && \
make install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
LD_LIBRARY_PATH="${PREFIX}/lib:${PREFIX}/lib64:${LD_LIBRARY_PATH}" ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/* /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g; s:/lib64:/lib:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM runtime-base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
# copy only needed files, without copying nvidia dev files
COPY --from=build /usr/local/bin /usr/local/bin/
COPY --from=build /usr/local/share /usr/local/share/
COPY --from=build /usr/local/lib /usr/local/lib/
COPY --from=build /usr/local/include /usr/local/include/
# Let's make sure the app built correctly
# Convenient to verify on https://hub.docker.com/r/jrottenberg/ffmpeg/builds/ console output

View File

@@ -1,490 +0,0 @@
# inspired by:
# https://github.com/collelog/ffmpeg/blob/master/4.3.1-alpine-rpi4-arm64v8.Dockerfile
# https://github.com/mmastrac/ffmpeg-omx-rpi-docker/blob/master/Dockerfile
# https://github.com/jrottenberg/ffmpeg/pull/158/files
# https://github.com/jrottenberg/ffmpeg/pull/239
FROM ubuntu:20.04 AS base
WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \
FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \
LIBPTHREAD_STUBS_VERSION=0.4 \
LIBVIDSTAB_VERSION=1.1.0 \
LIBXCB_VERSION=1.13.1 \
XCBPROTO_VERSION=1.13 \
OGG_VERSION=1.3.2 \
OPENCOREAMR_VERSION=0.1.5 \
OPUS_VERSION=1.2 \
OPENJPEG_VERSION=2.1.2 \
THEORA_VERSION=1.1.1 \
VORBIS_VERSION=1.3.5 \
VPX_VERSION=1.8.0 \
WEBP_VERSION=1.0.2 \
X264_VERSION=20170226-2245-stable \
X265_VERSION=3.1.1 \
XAU_VERSION=1.0.9 \
XORG_MACROS_VERSION=1.19.2 \
XPROTO_VERSION=7.0.31 \
XVID_VERSION=1.3.4 \
LIBZMQ_VERSION=4.3.3 \
SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
ARG OPUS_SHA256SUM="77db45a87b51578fbc49555ef1b10926179861d854eb2613207dc79d9ec0a9a9 opus-1.2.tar.gz"
ARG THEORA_SHA256SUM="40952956c47811928d1e7922cda3bc1f427eb75680c3c37249c91e949054916b libtheora-1.1.1.tar.gz"
ARG VORBIS_SHA256SUM="6efbcecdd3e5dfbf090341b485da9d176eb250d893e3eb378c428a2db38301ce libvorbis-1.3.5.tar.gz"
ARG XVID_SHA256SUM="4e9fd62728885855bc5007fe1be58df42e5e274497591fec37249e1052ae316f xvidcore-1.3.4.tar.gz"
ARG LD_LIBRARY_PATH=/opt/ffmpeg/lib
ARG MAKEFLAGS="-j2"
ARG PKG_CONFIG_PATH="/opt/ffmpeg/share/pkgconfig:/opt/ffmpeg/lib/pkgconfig:/opt/ffmpeg/lib64/pkgconfig:/opt/vc/lib/pkgconfig"
ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib:/opt/vc/lib"
RUN buildDeps="autoconf \
automake \
cmake \
curl \
bzip2 \
libexpat1-dev \
g++ \
gcc \
git \
gperf \
libtool \
make \
nasm \
perl \
pkg-config \
python \
sudo \
libssl-dev \
yasm \
linux-headers-raspi2 \
libomxil-bellagio-dev \
libx265-dev \
libaom-dev \
zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
RUN \
DIR=/tmp/opencore-amr && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-${OPENCOREAMR_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## x264 http://www.videolan.org/developers/x264.html
RUN \
DIR=/tmp/x264 && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://download.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \
tar -jx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared --enable-pic --disable-cli && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
# ### x265 http://x265.org/
# RUN \
# DIR=/tmp/x265 && \
# mkdir -p ${DIR} && \
# cd ${DIR} && \
# curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \
# tar -zx && \
# cd x265_${X265_VERSION}/build/linux && \
# sed -i "/-DEXTRA_LIB/ s/$/ -DCMAKE_INSTALL_PREFIX=\${PREFIX}/" multilib.sh && \
# sed -i "/^cmake/ s/$/ -DENABLE_CLI=OFF/" multilib.sh && \
# # export CXXFLAGS="${CXXFLAGS} -fPIC" && \
# ./multilib.sh && \
# make -C 8bit install && \
# rm -rf ${DIR}
### libogg https://www.xiph.org/ogg/
RUN \
DIR=/tmp/ogg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz && \
echo ${OGG_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libogg-${OGG_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libopus https://www.opus-codec.org/
RUN \
DIR=/tmp/opus && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://archive.mozilla.org/pub/opus/opus-${OPUS_VERSION}.tar.gz && \
echo ${OPUS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f opus-${OPUS_VERSION}.tar.gz && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvorbis https://xiph.org/vorbis/
RUN \
DIR=/tmp/vorbis && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz && \
echo ${VORBIS_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libvorbis-${VORBIS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libtheora http://www.theora.org/
RUN \
DIR=/tmp/theora && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.gz && \
echo ${THEORA_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f libtheora-${THEORA_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --prefix="${PREFIX}" --with-ogg="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libvpx https://www.webmproject.org/code/
RUN \
DIR=/tmp/vpx && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-vp8 --enable-vp9 --enable-vp9-highbitdepth --enable-pic --enable-shared \
--disable-debug --disable-examples --disable-docs --disable-install-bins && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libwebp https://developers.google.com/speed/webp/
RUN \
DIR=/tmp/vebp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${WEBP_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### libmp3lame http://lame.sourceforge.net/
RUN \
DIR=/tmp/lame && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://versaweb.dl.sourceforge.net/project/lame/lame/$(echo ${LAME_VERSION} | sed -e 's/[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z-]*\)/\1.\2/')/lame-${LAME_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" --enable-shared --enable-nasm --disable-frontend && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### xvid https://www.xvid.com/
RUN \
DIR=/tmp/xvid && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz && \
echo ${XVID_SHA256SUM} | sha256sum --check && \
tar -zx -f xvidcore-${XVID_VERSION}.tar.gz && \
cd xvidcore/build/generic && \
./configure --prefix="${PREFIX}" --bindir="${PREFIX}/bin" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
### fdk-aac https://github.com/mstorsjo/fdk-aac
RUN \
DIR=/tmp/fdk-aac && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
autoreconf -fiv && \
./configure --prefix="${PREFIX}" --enable-shared --datadir="${DIR}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## openjpeg https://github.com/uclouvain/openjpeg
RUN \
DIR=/tmp/openjpeg && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sL https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz | \
tar -zx --strip-components=1 && \
export CFLAGS="${CFLAGS} -DPNG_ARM_NEON_OPT=0" && \
cmake -DBUILD_THIRDPARTY:BOOL=ON -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## freetype https://www.freetype.org/
RUN \
DIR=/tmp/freetype && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://download.savannah.gnu.org/releases/freetype/freetype-${FREETYPE_VERSION}.tar.gz && \
echo ${FREETYPE_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f freetype-${FREETYPE_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libvstab https://github.com/georgmartius/vid.stab
RUN \
DIR=/tmp/vid.stab && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/georgmartius/vid.stab/archive/v${LIBVIDSTAB_VERSION}.tar.gz && \
echo ${LIBVIDSTAB_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f v${LIBVIDSTAB_VERSION}.tar.gz && \
cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" . && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## fridibi https://www.fribidi.org/
RUN \
DIR=/tmp/fribidi && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/fribidi/fribidi/archive/${FRIBIDI_VERSION}.tar.gz && \
echo ${FRIBIDI_SHA256SUM} | sha256sum --check && \
tar -zx --strip-components=1 -f ${FRIBIDI_VERSION}.tar.gz && \
sed -i 's/^SUBDIRS =.*/SUBDIRS=gen.tab charset lib bin/' Makefile.am && \
./bootstrap --no-config --auto && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j1 && \
make -j $(nproc) install && \
rm -rf ${DIR}
## kvazaar https://github.com/ultravideo/kvazaar
RUN \
DIR=/tmp/kvazaar && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/ultravideo/kvazaar/archive/v${KVAZAAR_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f v${KVAZAAR_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
# RUN \
# DIR=/tmp/aom && \
# git clone --branch ${AOM_VERSION} --depth 1 https://aomedia.googlesource.com/aom ${DIR} ; \
# cd ${DIR} ; \
# rm -rf CMakeCache.txt CMakeFiles ; \
# mkdir -p ./aom_build ; \
# cd ./aom_build ; \
# cmake -DCMAKE_INSTALL_PREFIX="${PREFIX}" -DBUILD_SHARED_LIBS=1 ..; \
# make ; \
# make install ; \
# rm -rf ${DIR}
## libxcb (and supporting libraries) for screen capture https://xcb.freedesktop.org/
RUN \
DIR=/tmp/xorg-macros && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive//individual/util/util-macros-${XORG_MACROS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f util-macros-${XORG_MACROS_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/xproto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/proto/xproto-${XPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xproto-${XPROTO_VERSION}.tar.gz && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess && \
curl -sL 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libXau && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://www.x.org/archive/individual/lib/libXau-${XAU_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libXau-${XAU_VERSION}.tar.gz && \
./configure --srcdir=${DIR} --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libpthread-stubs && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libpthread-stubs-${LIBPTHREAD_STUBS_VERSION}.tar.gz && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb-proto && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f xcb-proto-${XCBPROTO_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
RUN \
DIR=/tmp/libxcb && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://xcb.freedesktop.org/dist/libxcb-${LIBXCB_VERSION}.tar.gz && \
tar -zx --strip-components=1 -f libxcb-${LIBXCB_VERSION}.tar.gz && \
ACLOCAL_PATH="${PREFIX}/share/aclocal" ./autogen.sh && \
./configure --prefix="${PREFIX}" --disable-static --enable-shared && \
make -j $(nproc) && \
make -j $(nproc) install && \
rm -rf ${DIR}
## libzmq https://github.com/zeromq/libzmq/
RUN \
DIR=/tmp/libzmq && \
mkdir -p ${DIR} && \
cd ${DIR} && \
curl -sLO https://github.com/zeromq/libzmq/archive/v${LIBZMQ_VERSION}.tar.gz && \
tar -xz --strip-components=1 -f v${LIBZMQ_VERSION}.tar.gz && \
./autogen.sh && \
./configure --prefix="${PREFIX}" && \
make -j $(nproc) && \
# make check && \
make -j $(nproc) install && \
rm -rf ${DIR}
## userland https://github.com/raspberrypi/userland
RUN \
DIR=/tmp/userland && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone --depth 1 https://github.com/raspberrypi/userland.git . && \
./buildme && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
curl -sLO https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 && \
tar -jx --strip-components=1 -f ffmpeg-${FFMPEG_VERSION}.tar.bz2
RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
./configure \
--disable-debug \
--disable-doc \
--disable-ffplay \
--enable-shared \
--enable-avresample \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
--enable-gpl \
--enable-libfreetype \
--enable-libvidstab \
--enable-libmp3lame \
--enable-libopus \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libxcb \
--enable-libx265 \
--enable-libxvid \
--enable-libx264 \
--enable-nonfree \
--enable-openssl \
--enable-libfdk_aac \
--enable-postproc \
--enable-small \
--enable-version3 \
--enable-libzmq \
--extra-libs=-ldl \
--prefix="${PREFIX}" \
--enable-libopenjpeg \
--enable-libkvazaar \
--enable-libaom \
--extra-libs=-lpthread \
--enable-omx \
--enable-omx-rpi \
--enable-mmal \
--enable-v4l2_m2m \
--enable-neon \
--extra-cflags="-I${PREFIX}/include" \
--extra-ldflags="-L${PREFIX}/lib" && \
make -j $(nproc) && \
make -j $(nproc) install && \
make tools/zmqsend && cp tools/zmqsend ${PREFIX}/bin/ && \
make distclean && \
hash -r && \
cd tools && \
make qt-faststart && cp qt-faststart ${PREFIX}/bin/
## cleanup
RUN \
ldd ${PREFIX}/bin/ffmpeg | grep opt/ffmpeg | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
# copy userland lib too
ldd ${PREFIX}/bin/ffmpeg | grep opt/vc | cut -d ' ' -f 3 | xargs -i cp {} /usr/local/lib/ && \
for lib in /usr/local/lib/*.so.*; do ln -s "${lib##*/}" "${lib%%.so.*}".so; done && \
cp ${PREFIX}/bin/* /usr/local/bin/ && \
cp -r ${PREFIX}/share/ffmpeg /usr/local/share/ && \
LD_LIBRARY_PATH=/usr/local/lib ffmpeg -buildconf && \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64:/usr/lib:/usr/lib64:/lib:/lib64
RUN \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends libx265-dev libaom-dev && \
apt-get autoremove -y && \
apt-get clean -y
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/

View File

@@ -1,52 +0,0 @@
FROM ubuntu:20.04 AS base
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
apt-get autoremove -y && \
apt-get clean -y
FROM base as build
ARG NGINX_VERSION=1.18.0
ARG VOD_MODULE_VERSION=1.28
ARG SECURE_TOKEN_MODULE_VERSION=1.4
ARG RTMP_MODULE_VERSION=1.2.1
RUN cp /etc/apt/sources.list /etc/apt/sources.list~ \
&& sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list \
&& apt-get update
RUN apt-get -yqq build-dep nginx
RUN apt-get -yqq install --no-install-recommends curl \
&& mkdir /tmp/nginx \
&& curl -sL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -C /tmp/nginx -zx --strip-components=1 \
&& mkdir /tmp/nginx-vod-module \
&& curl -sL https://github.com/kaltura/nginx-vod-module/archive/refs/tags/${VOD_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-vod-module -zx --strip-components=1 \
# Patch MAX_CLIPS to allow more clips to be added than the default 128
&& sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h \
&& mkdir /tmp/nginx-secure-token-module \
&& curl -sL https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-secure-token-module -zx --strip-components=1 \
&& mkdir /tmp/nginx-rtmp-module \
&& curl -sL https://github.com/arut/nginx-rtmp-module/archive/refs/tags/v${RTMP_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-rtmp-module -zx --strip-components=1
WORKDIR /tmp/nginx
RUN ./configure --prefix=/usr/local/nginx \
--with-file-aio \
--with-http_sub_module \
--with-http_ssl_module \
--with-threads \
--add-module=../nginx-vod-module \
--add-module=../nginx-secure-token-module \
--add-module=../nginx-rtmp-module \
--with-cc-opt="-O3 -Wno-error=implicit-fallthrough"
RUN make && make install
RUN rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default
FROM base
COPY --from=build /usr/local/nginx /usr/local/nginx
ENTRYPOINT ["/usr/local/nginx/sbin/nginx"]
CMD ["-g", "daemon off;"]

View File

@@ -1,9 +0,0 @@
ARG NODE_VERSION=14.0
FROM node:${NODE_VERSION}
WORKDIR /opt/frigate
COPY . .
RUN npm install && npm run build

View File

@@ -1,41 +0,0 @@
FROM ubuntu:20.04 as build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -qq update \
&& apt-get -qq install -y \
python3 \
python3-dev \
wget \
# opencv dependencies
build-essential cmake git pkg-config libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
# scipy dependencies
gcc gfortran libopenblas-dev liblapack-dev cython
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& python3 get-pip.py "pip==20.2.4"
RUN pip3 install scikit-build
RUN pip3 wheel --wheel-dir=/wheels \
opencv-python-headless \
numpy \
imutils \
scipy \
psutil \
Flask \
paho-mqtt \
PyYAML \
matplotlib \
click \
setproctitle \
peewee
FROM scratch
COPY --from=build /wheels /wheels

View File

@@ -30,17 +30,17 @@ http {
gzip_vary on;
upstream frigate_api {
server localhost:5001;
server 127.0.0.1:5001;
keepalive 1024;
}
upstream mqtt_ws {
server localhost:5002;
server 127.0.0.1:5002;
keepalive 1024;
}
upstream jsmpeg {
server localhost:8082;
server 127.0.0.1:8082;
keepalive 1024;
}
@@ -58,7 +58,7 @@ http {
# vod caches
vod_metadata_cache metadata_cache 512m;
vod_mapping_cache mapping_cache 5m;
vod_mapping_cache mapping_cache 5m 10m;
# gzip manifests
gzip on;
@@ -81,11 +81,13 @@ http {
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
add_header Access-Control-Allow-Origin '*';
expires -1;
add_header Cache-Control "no-store";
expires off;
}
location /stream/ {
add_header 'Cache-Control' 'no-cache';
add_header Cache-Control "no-store";
expires off;
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
@@ -170,10 +172,22 @@ http {
proxy_set_header Host $host;
}
location /api/ {
location ~* /api/(.*\.(jpg|jpeg|png)$) {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
proxy_pass http://frigate_api/$1$is_args$args;
proxy_pass_request_headers on;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/ {
add_header Cache-Control "no-store";
expires off;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
proxy_pass http://frigate_api/;
proxy_pass_request_headers on;
proxy_set_header Host $host;
@@ -181,21 +195,23 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
add_header Cache-Control "no-cache";
add_header Cache-Control "no-store";
expires off;
location ~* \.(?:js|css|svg|ico|png)$ {
location /assets/ {
access_log off;
expires 1y;
add_header Cache-Control "public";
}
sub_filter 'href="/' 'href="$http_x_ingress_path/';
sub_filter 'url(/' 'url($http_x_ingress_path/';
sub_filter '"/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/js/' '"$http_x_ingress_path/js/';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
sub_filter '="/BASE_PATH/"' '=window.baseUrl';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
sub_filter_types text/css application/javascript;
sub_filter_once off;

View File

@@ -43,8 +43,27 @@ If you are storing your database on a network share (SMB, NFS, etc), you may get
This may need to be in a custom location if network storage is used for the media folder.
```yaml
database:
path: /path/to/frigate.db
```
### `model`
If using a custom model, the width and height will need to be specified.
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
```yaml
model:
labelmap:
2: vehicle
3: vehicle
5: vehicle
7: vehicle
15: animal
16: animal
17: animal
```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.

View File

@@ -0,0 +1,14 @@
# Birdseye
Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
### Birdseye Modes
Birdseye offers different modes to customize which cameras show under which circumstances.
- **continuous:** All cameras are always included
- **motion:** Cameras that have detected motion within the last 30 seconds are included
- **objects:** Cameras that have tracked an active object within the last 30 seconds are included
### Custom Birdseye Icon
A custom icon can be added to the birdseye background by provided a file `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view.

View File

@@ -19,7 +19,35 @@ output_args:
rtmp: -c:v libx264 -an -f flv
```
### RTMP Cameras (Reolink 410/520 and possibly others)
### JPEG Stream Cameras
Cameras using a live changing jpeg image will need input parameters as below
```yaml
input_args:
- -r
- 5 # << enter FPS here
- -stream_loop
- -1
- -f
- image2
- -avoid_negative_ts
- make_zero
- -fflags
- nobuffer
- -flags
- low_delay
- -strict
- experimental
- -fflags
- +genpts+discardcorrupt
- -use_wallclock_as_timestamps
- 1
```
Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras)
### RTMP Cameras
The input parameters need to be adjusted for RTMP cameras
@@ -28,13 +56,53 @@ ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv
```
### Reolink 410/520 (possibly others)
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink.
```yaml
cameras:
reolink:
ffmpeg:
hwaccel_args:
input_args:
- -avoid_negative_ts
- make_zero
- -fflags
- nobuffer+genpts+discardcorrupt
- -flags
- low_delay
- -strict
- experimental
- -analyzeduration
- 1000M
- -probesize
- 1000M
- -rw_timeout
- "5000000"
inputs:
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password
roles:
- record
- rtmp
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password
roles:
- detect
detect:
width: 896
height: 672
fps: 7
```
![Resolutions](/img/reolink-settings.png)
### Blue Iris RTSP Cameras
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
```
### UDP Only Cameras
@@ -43,5 +111,5 @@ If your cameras do not support TCP connections for RTSP, you can use UDP.
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 5000000 -use_wallclock_as_timestamps 1
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1
```

View File

@@ -31,6 +31,7 @@ detectors:
```
### Native Coral (Dev Board)
_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_
```yaml
detectors:

View File

@@ -5,51 +5,29 @@ title: Hardware Acceleration
It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
### Raspberry Pi 3/4 (32-bit OS)
### Raspberry Pi 3/4
Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config > Performance Options > GPU Memory).
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
```yaml
ffmpeg:
hwaccel_args:
- -c:v
- h264_mmal
```
### Raspberry Pi 3/4 (64-bit OS)
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
```yaml
ffmpeg:
hwaccel_args:
- -c:v
- h264_v4l2m2m
hwaccel_args: -c:v h264_v4l2m2m
```
### Intel-based CPUs (<10th Generation) via Quicksync
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
- -hwaccel_output_format
- yuv420p
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
```
**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME_JELLYFIN=i965` to your docker-compose file.
### Intel-based CPUs (>=10th Generation) via Quicksync
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- qsv
- -qsv_device
- /dev/dri/renderD128
hwaccel_args: -c:v h264_qsv
```
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
@@ -58,13 +36,81 @@ ffmpeg:
```yaml
ffmpeg:
hwaccel_args:
- -hwaccel
- vaapi
- -hwaccel_device
- /dev/dri/renderD128
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
```
### NVIDIA GPU
NVIDIA GPU based decoding via NVDEC is supported, but requires special configuration. See the [NVIDIA NVDEC documentation](/configuration/nvdec) for more details.
These instructions are based on the [jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux)
Add `--gpus all` to your docker run command or update your compose file.
```yaml
services:
frigate:
...
image: blakeblackshear/frigate:stable
deploy: # <------------- Add this section
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
The decoder you need to pass in the `hwaccel_args` will depend on the input video.
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
```shell
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
For example, for H264 video, you'll select `h264_cuvid`.
```yaml
ffmpeg:
hwaccel_args: -c:v h264_cuvid
```
If everything is working correctly, you should see a significant improvement in performance.
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
processes:
```
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 12737 C ffmpeg 249MiB |
| 0 N/A N/A 12751 C ffmpeg 249MiB |
| 0 N/A N/A 12772 C ffmpeg 249MiB |
| 0 N/A N/A 12775 C ffmpeg 249MiB |
| 0 N/A N/A 12800 C ffmpeg 249MiB |
| 0 N/A N/A 12811 C ffmpeg 417MiB |
| 0 N/A N/A 12827 C ffmpeg 417MiB |
+-----------------------------------------------------------------------------+
```

View File

@@ -48,8 +48,8 @@ mqtt:
# Optional: user
user: mqtt_user
# Optional: password
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
# e.g. password: '{FRIGATE_MQTT_PASSWORD}'
password: password
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
tls_ca_certs: /path/to/ca.crt
@@ -110,6 +110,7 @@ environment_vars:
EXAMPLE_VAR: value
# Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level
birdseye:
# Optional: Enable birdseye view (default: shown below)
enabled: True
@@ -134,7 +135,7 @@ ffmpeg:
# NOTE: See hardware acceleration docs for your specific device
hwaccel_args: []
# Optional: global input args (default: shown below)
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
# Optional: global output args
output_args:
# Optional: output args for detect streams (default: shown below)
@@ -159,6 +160,27 @@ detect:
enabled: True
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25
# Optional: Configuration for stationary object tracking
stationary:
# Optional: Frequency for confirming stationary objects (default: shown below)
# When set to 0, object detection will not confirm stationary objects until movement is detected.
# If set to 10, object detection will run to confirm the object still exists on every 10th frame.
interval: 0
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
threshold: 50
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
# This can help with false positives for objects that should only be stationary for a limited amount of time.
# It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave
# car at the default.
# WARNING: Setting these values overrides default behavior and disables stationary object tracking.
# There are very few situations where you would want it disabled. It is NOT recommended to
# copy these values from the example config into your config unless you know they are needed.
max_frames:
# Optional: Default for all object types (default: not set, track forever)
default: 3000
# Optional: Object specific values
objects:
person: 1000
# Optional: Object configuration
# NOTE: Can be overridden at the camera level
@@ -177,6 +199,10 @@ objects:
min_area: 5000
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
max_area: 100000
# Optional: minimum width/height of the bounding box for the detected object (default: 0)
min_ratio: 0.5
# Optional: maximum width/height of the bounding box for the detected object (default: 24000000)
max_ratio: 2.0
# Optional: minimum score for the object to initiate tracking (default: shown below)
min_score: 0.5
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
@@ -192,10 +218,14 @@ motion:
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
# The value should be between 1 and 255.
threshold: 25
# Optional: Minimum size in pixels in the resized motion image that counts as motion (default: ~0.17% of the motion frame area)
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller
# moving objects.
contour_area: 100
# Optional: Minimum size in pixels in the resized motion image that counts as motion (default: 30)
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will
# make motion detection more sensitive to smaller moving objects.
# As a rule of thumb:
# - 15 - high sensitivity
# - 30 - medium sensitivity
# - 50 - low sensitivity
contour_area: 30
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
# Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
# Too low and a fast moving person wont be detected as motion.
@@ -205,29 +235,49 @@ motion:
# Low values will cause things like moving shadows to be detected as motion for longer.
# https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
frame_alpha: 0.2
# Optional: Height of the resized motion frame (default: 1/6th of the original frame height, but no less than 180)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage.
# Lower values result in less CPU, but small changes may not register as motion.
frame_height: 180
# Optional: Height of the resized motion frame (default: 50)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense
# of higher CPU usage. Lower values result in less CPU, but small changes may not register as motion.
frame_height: 50
# Optional: motion mask
# NOTE: see docs for more detailed info on creating masks
mask: 0,900,1080,900,1080,1920,0,1920
# Optional: improve contrast (default: shown below)
# Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive
# for daytime.
improve_contrast: False
# Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below).
mqtt_off_delay: 30
# Optional: Record configuration
# NOTE: Can be overridden at the camera level
record:
# Optional: Enable recording (default: shown below)
# WARNING: If recording is disabled in the config, turning it on via
# the UI or MQTT later will have no effect.
# WARNING: Frigate does not currently support limiting recordings based
# on available disk space automatically. If using recordings,
# you must specify retention settings for a number of days that
# will fit within the available disk space of your drive or Frigate
# will crash.
enabled: False
# Optional: Number of days to retain recordings regardless of events (default: shown below)
# NOTE: This should be set to 0 and retention should be defined in events section below
# if you only want to retain recordings of events.
retain_days: 0
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
expire_interval: 60
# Optional: Retention settings for recording
retain:
# Optional: Number of days to retain recordings regardless of events (default: shown below)
# NOTE: This should be set to 0 and retention should be defined in events section below
# if you only want to retain recordings of events.
days: 0
# Optional: Mode for retention. Available options are: all, motion, and active_objects
# all - save all recording segments regardless of activity
# motion - save all recordings segments with any detected motion
# active_objects - save all recording segments with active/moving objects
# NOTE: this mode only applies when the days setting above is greater than 0
mode: all
# Optional: Event recording settings
events:
# Optional: Maximum length of time to retain video during long events. (default: shown below)
# NOTE: If an object is being tracked for longer than this amount of time, the retained recordings
# will be the last x seconds of the event unless retain_days under record is > 0.
max_seconds: 300
# Optional: Number of seconds before the event to include (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include (default: shown below)
@@ -241,6 +291,16 @@ record:
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Mode for retention. (default: shown below)
# all - save all recording segments for events regardless of activity
# motion - save all recordings segments for events with any detected motion
# active_objects - save all recording segments for event with active/moving objects
#
# NOTE: If the retain mode for the camera is more restrictive than the mode configured
# here, the segments will already be gone by the time this mode is applied.
# For example, if the camera retain mode is "motion", the segments without motion are
# never stored, so setting the mode to "all" here won't bring them back.
mode: motion
# Optional: Per object retention days
objects:
person: 15
@@ -319,7 +379,7 @@ cameras:
# Required: A list of input streams for the camera. See documentation for more information.
inputs:
# 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
# Required: list of roles for this stream. valid values are: detect,record,rtmp
# NOTICE: In addition to assigning the record, and rtmp roles,
@@ -353,7 +413,7 @@ cameras:
# camera.
front_steps:
# Required: List of x,y coordinates to define the polygon of the zone.
# NOTE: Coordinates can be generated at https://www.image-map.net/
# NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box.
coordinates: 545,1077,747,939,788,805
# Optional: List of objects that can trigger this zone (default: all tracked objects)
objects:
@@ -384,4 +444,12 @@ cameras:
quality: 70
# Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Configuration for how camera is handled in the GUI.
ui:
# Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
# By default the cameras are sorted alphabetically.
order: 0
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True
```

View File

@@ -36,7 +36,7 @@ motion:
- 0,461,3,0,1919,0,1919,843,1699,492,1344
```
![poly](/img/example-mask-poly.png)
![poly](/img/example-mask-poly-min.png)
### Further Clarification

View File

@@ -1,109 +0,0 @@
---
id: nvdec
title: NVIDIA hardware decoder
---
Certain nvidia cards include a hardware decoder, which can greatly improve the
performance of video decoding. In order to use NVDEC, a special build of
ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvidia'
includes this support for amd64 platforms. An aarch64 for the Jetson, which
also includes NVDEC may be added in the future.
## Docker setup
### Requirements
[nVidia closed source driver](https://www.nvidia.com/en-us/drivers/unix/) required to access NVDEC.
[nvidia-docker](https://github.com/NVIDIA/nvidia-docker) required to pass NVDEC to docker.
### Setting up docker-compose
In order to pass NVDEC, the docker engine must be set to `nvidia` and the environment variables
`NVIDIA_VISIBLE_DEVICES=all` and `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` must be set.
In a docker compose file, these lines need to be set:
```yaml
services:
frigate:
...
image: blakeblackshear/frigate:stable-amd64nvidia
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
```
### Setting up the configuration file
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
The decoder you choose will depend on the input video.
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
```shell
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
`-c:v hevc_cuvid` to your ffmpeg input arguments:
```yaml
ffmpeg:
input_args: ...
- -c:v
- hevc_cuvid
```
If everything is working correctly, you should see a significant improvement in performance.
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
processes:
```
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 12737 C ffmpeg 249MiB |
| 0 N/A N/A 12751 C ffmpeg 249MiB |
| 0 N/A N/A 12772 C ffmpeg 249MiB |
| 0 N/A N/A 12775 C ffmpeg 249MiB |
| 0 N/A N/A 12800 C ffmpeg 249MiB |
| 0 N/A N/A 12811 C ffmpeg 417MiB |
| 0 N/A N/A 12827 C ffmpeg 417MiB |
+-----------------------------------------------------------------------------+
```
To further improve performance, you can set ffmpeg to skip frames in the output,
using the fps filter:
```yaml
output_args:
- -filter:v
- fps=fps=5
```
This setting, for example, allows Frigate to consume my 10-15fps camera streams on
my relatively low powered Haswell machine with relatively low cpu usage.

View File

@@ -5,7 +5,11 @@ title: Objects
import labels from "../../../labelmap.txt";
By default, Frigate includes the following object models from the Google Coral test data. Note that `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
Frigate includes the object models listed below from the Google Coral test data.
Please note:
- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
- `person` is the only tracked object by default. See the [full configuration reference](https://docs.frigate.video/configuration/index#full-configuration-reference) for an example of expanding the list of tracked objects.
<ul>
{labels.split("\n").map((label) => (

View File

@@ -14,12 +14,31 @@ If you only used clips in previous versions with recordings disabled, you can us
```yaml
record:
enabled: True
retain_days: 0
events:
retain:
default: 10
```
This configuration will retain recording segments that overlap with events for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
This configuration will retain recording segments that overlap with events and have active tracked objects for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
When `retain_days` is set to `0`, events will have up to `max_seconds` (defaults to 5 minutes) of recordings retained. Increasing `retain_days` to `1` will allow events to exceed the `max_seconds` limitation of up to 1 day.
When `retain -> days` is set to `0`, segments will be deleted from the cache if no events are in progress.
## Can I have "24/7" recordings, but only at certain times?
Using Frigate UI, HomeAssistant, or MQTT, cameras can be automated to only record in certain situations or at certain times.
**WARNING**: Recordings still must be enabled in the config. If a camera has recordings disabled in the config, enabling via the methods listed above will have no effect.
## What do the different retain modes mean?
Frigate saves from the stream with the `record` role in 10 second segments. These options determine which recording segments are kept for 24/7 recording (but can also affect events).
Let's say you have frigate configured so that your doorbell camera would retain the last **2** days of 24/7 recording.
- With the `all` option all 48 hours of those two days would be kept and viewable.
- With the `motion` option the only parts of those 48 hours would be segments that frigate detected motion. This is the middle ground option that won't keep all 48 hours, but will likely keep all segments of interest along with the potential for some extra segments.
- With the `active_objects` option the only segments that would be kept are those where there was a true positive object that was not considered stationary.
The same options are available with events. Let's consider a scenario where you drive up and park in your driveway, go inside, then come back out 4 hours later.
- With the `all` option all segments for the duration of the event would be saved for the event. This event would have 4 hours of footage.
- With the `motion` option all segments for the duration of the event with motion would be saved. This means any segment where a car drove by in the street, person walked by, lighting changed, etc. would be saved.
- With the `active_objects` it would only keep segments where the object was active. In this case the only segments that would be saved would be the ones where the car was driving up, you going inside, you coming outside, and the car driving away. Essentially reducing the 4 hours to a minute or two of event footage.

View File

@@ -5,4 +5,4 @@ title: RTMP
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization.
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](../faqs#audio-in-recordings).

View File

@@ -0,0 +1,28 @@
# Stationary Objects
An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs near the object at which point object detection will start running again. If the object changes location, it will be considered active.
## Why does it matter if an object is stationary?
Once an object becomes stationary, object detection will not be continually run on that object. This serves to reduce resource usage and redundant detections when there has been no motion near the tracked object. This also means that Frigate is contextually aware, and can for example [filter out recording segments](record.md#what-do-the-different-retain-modes-mean) to only when the object is considered active. Motion alone does not determine if an object is "active" for active_objects segment retention. Lighting changes for a parked car won't make an object active.
## Tuning stationary behavior
The default config is:
```yaml
detect:
stationary:
interval: 0
threshold: 50
```
`interval` is defined as the frequency for running detection on stationary objects. This means that by default once an object is considered stationary, detection will not be run on it until motion is detected. With `interval > 0`, every nth frames detection will be run to make sure the object is still there.
NOTE: There is no way to disable stationary object tracking with this value.
`threshold` is the number of frames an object needs to remain relatively still before it is considered stationary.
## Avoiding stationary objects
In some cases, like a driveway, you may prefer to only have an event when a car is coming & going vs a constant event of it stationary in the driveway. [This docs sections](../guides/stationary_objects.md) explains how to approach that scenario.

View File

@@ -0,0 +1,15 @@
---
id: user_interface
title: User Interface Configurations
---
### Experimental UI
While developing and testing new components, users may decide to opt-in to test potential new features on the front-end.
```yaml
ui:
use_experimental: true
```
Note that experimental changes may contain bugs or may be removed at any time in future releases of the software. Use of these features are presented as-is and with no functional guarantee.

View File

@@ -3,7 +3,9 @@ id: zones
title: Zones
---
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Presence in a zone is evaluated based on the bottom center of the bounding box for the object. It does not matter how much of the bounding box overlaps with the zone.
Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
During testing, enable the Zones option for the debug feed so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
@@ -20,6 +22,10 @@ camera:
required_zones:
- entire_yard
- front_yard_street
snapshots:
required_zones:
- entire_yard
- front_yard_street
zones:
entire_yard:
coordinates: ... (everywhere you want a person)
@@ -31,4 +37,4 @@ camera:
- car
```
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get clips for person objects that enter anywhere in the yard, and clips for cars only if they enter the street.
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.

View File

@@ -40,9 +40,7 @@ Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshe
### Setup
#### 1. Build the docker container locally with the appropriate make command
For x86 machines, use `make amd64_frigate`
#### 1. Build the version information and docker container locally by running `make`
#### 2. Create a local config file for testing
@@ -90,6 +88,38 @@ VSCode will start the docker compose file for you and open a terminal window con
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
### Testing
#### FFMPEG Hardware Acceleration
The following commands are used inside the container to ensure hardware acceleration is working properly.
**Raspberry Pi (64bit)**
This should show <50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`.
```shell
ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**NVIDIA**
```shell
ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**VAAPI**
```shell
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
**QSV**
```shell
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
```
## Web Interface
### Prerequisites
@@ -117,20 +147,16 @@ cd web && npm install
#### 3. Run the development server
```console
cd web && npm run start
cd web && npm run dev
```
#### 3a. Run the development server against a non-local instance
To run the development server against a non-local instance, you will need to provide an environment variable, `SNOWPACK_PUBLIC_API_HOST` that tells the web application how to connect to the Frigate API:
```console
cd web && SNOWPACK_PUBLIC_API_HOST=http://<ip-address-to-your-frigate-instance>:5000 npm run start
```
To run the development server against a non-local instance, you will need to modify the API_HOST default return in `web/src/env.js`.
#### 4. Making changes
The Web UI is built using [Snowpack](https://www.snowpack.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
Light guidelines and advice:
@@ -182,3 +208,16 @@ npm run build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Official builds
Setup buildx for multiarch
```
docker buildx stop builder && docker buildx rm builder # <---- if existing
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name builder --driver docker-container --driver-opt network=host --use
docker buildx inspect builder --bootstrap
make build_web
make push
```

View File

@@ -11,15 +11,30 @@ This error message is due to a shm-size that is too small. Try updating your shm
A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
### How can I get sound or audio in my recordings?
### How can I get sound or audio in my recordings? {#audio-in-recordings}
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#ffmpeg).
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).
:::tip
When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example:
```diff title="frigate.yml"
ffmpeg:
output_args:
- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac
```
This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`.
:::
### 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.
![mismatched-resolution](/img/mismatched-resolution.jpg)
![mismatched-resolution](/img/mismatched-resolution-min.jpg)
### I can't view events or recordings in the Web UI.

View File

@@ -17,7 +17,7 @@ The ideal resolution for detection is one where the objects you want to detect f
Larger resolutions **do** improve performance if the objects are very small in the frame.
![Resolutions](/img/resolutions.png)
![Resolutions](/img/resolutions-min.jpg)
### Example Camera Configuration

View File

@@ -3,7 +3,11 @@ id: false_positives
title: Reducing false positives
---
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_score`, `threshold`.
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_ratio`, `max_ratio`, `min_score`, `threshold`.
The `min_area` and `max_area` values are compared against the area (number of pixels) from a given detected object. If the area is outside this range, the object will be ignored as a false positive. This allows objects that must be too small or too large to be ignored.
Similarly, the `min_ratio` and `max_ratio` values are compared against a given detected object's width/height ratio (in pixels). If the ratio is outside this range, the object will be ignored as a false positive. This allows objects that are proportionally too short-and-wide (higher ratio) or too tall-and-narrow (smaller ratio) to be ignored.
For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:

View File

@@ -62,6 +62,8 @@ cameras:
roles:
- detect
- rtmp
rtmp:
enabled: False # <-- RTMP should be disabled if your stream is not H264
detect:
width: 1280 # <---- update for your camera's resolution
height: 720 # <---- update for your camera's resolution
@@ -71,7 +73,9 @@ cameras:
At this point you should be able to start Frigate and see the the video feed in the UI.
If you get a green image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with RTSP cameras that support TCP connections. FFmpeg arguments for other types of cameras can be found [here](/configuration/camera_specific).
If you get a green image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with H264 RTSP cameras that support TCP connections. If you do not have H264 cameras, make sure you have disabled RTMP. It is possible to enable it, but you must tell ffmpeg to re-encode the video with customized output args.
FFmpeg arguments for other types of cameras can be found [here](/configuration/camera_specific).
### Step 5: Configure hardware acceleration (optional)
@@ -163,13 +167,17 @@ cameras:
roles:
- detect
- rtmp
- record # <----- Add role
- path: rtsp://10.0.10.10:554/high_res_stream # <----- Add high res stream
roles:
- record
detect: ...
record: # <----- Enable recording
enabled: True
motion: ...
```
If you don't have separate streams for detect and record, you would just add the record role to the list on the first input.
By default, Frigate will retain video of all events for 10 days. The full set of options for recording can be found [here](/configuration/index#full-configuration-reference).
### Step 8: Enable snapshots (optional)

View File

@@ -25,6 +25,30 @@ automation:
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
Note that iOS devices support live previews of cameras by adding a camera entity id to the message data.
```yaml
automation:
- alias: Security_Frigate_Notifications
description: ""
trigger:
- platform: mqtt
topic: frigate/events
payload: new
value_template: "{{ value_json.type }}"
action:
- service: notify.mobile_app_iphone
data:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
image: >-
https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg
tag: '{{trigger.payload_json["after"]["id"]}}'
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
entity_id: camera.{{trigger.payload_json["after"]["camera"]}}
mode: single
```
## Conditions
Conditions with the `before` and `after` values allow a high degree of customization for automations.

View File

@@ -15,13 +15,13 @@ Frigate is designed to track objects as they move and over-masking can prevent i
:::
For example, you could create multiple zones that cover your driveway. For cars, you would only notify if entered_zones has more than 1 zone. For person, you would notify regardless of the number of entered_zones.
To only be notified of cars that enter your driveway from the street, you could create multiple zones that cover your driveway. For cars, you would only notify if `entered_zones` from the events MQTT topic has more than 1 zone.
See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation.
See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation to see how to restrict zones to certain object types.
You can also create a zone for the entrance of your driveway and only save an event if that zone is in the list of entered_zones when the object is a car.
![Driveway Zones](/img/driveway_zones-min.png)
![Driveway Zones](/img/driveway_zones.png)
To limit snapshots and events, you can list the zone for the entrance of your driveway under `required_zones` in your configuration file. Example below.
```yaml
camera:

View File

@@ -9,28 +9,29 @@ Cameras that output H.264 video and AAC audio will offer the most compatibility
I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options.
Many users have reported various issues with Reolink cameras, so I do not recommend them. If you are using Reolink, I suggest the [Reolink specific configuration](configuration/camera_specific#reolink-410520-possibly-others). Wifi cameras are also not recommended. Their streams are less reliable and cause connection loss and/or lost video data.
Here are some of the camera's I recommend:
- [Loryta(Dahua) T5442TM-AS-LED](https://amzn.to/2Wck2hQ) (affiliate link)
- [Loryta(Dahua) IPC-T5442TM-AS](https://amzn.to/39FODrm) (affiliate link)
- [Amcrest IP5M-T1179EW-28MM](https://amzn.to/39H1zgt) (affiliate link)
- <a href="https://amzn.to/3uFLtxB" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) T5442TM-AS-LED</a> (affiliate link)
- <a href="https://amzn.to/3isJ3gU" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) IPC-T5442TM-AS</a> (affiliate link)
- <a href="https://amzn.to/2ZWNWIA" target="_blank" rel="nofollow noopener sponsored">Amcrest IP5M-T1179EW-28MM</a> (affiliate link)
I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
## Server
My current favorite is the Minisforum GK50 because the dual NICs allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
My current favorite is the Minisforum GK41 because of the dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
| Name | Inference Speed | Notes |
| ------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [Minisforum GK41](https://amzn.to/3kI0njr) (affiliate link) | 9-10ms | Great alternative to a NUC. Easily handles several 1080p cameras. |
| [Minisforum GK50](https://amzn.to/3m49yKk) (affiliate link) | 9-10ms | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| [Intel NUC](https://amzn.to/3kImYMT) (affiliate link) | 8-10ms | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. |
| [BMAX B2 Plus](https://amzn.to/3uccBnD) (affiliate link) | 10-12ms | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
| [Atomic Pi](https://amzn.to/3i9YRVw) (affiliate link) | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| [Raspberry Pi 3B (32bit)](https://amzn.to/3lZUi16) (affiliate link) | 60ms | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. |
| [Raspberry Pi 4 (32bit)](https://amzn.to/2ZpgDNW) (affiliate link) | 15-20ms | Can handle a small number of cameras. The 2GB version runs fine. |
| [Raspberry Pi 4 (64bit)](https://amzn.to/2ZpgDNW) (affiliate link) | 10-15ms | Can handle a small number of cameras. The 2GB version runs fine. |
| Name | Inference Speed | Coral Compatibility | Notes |
| ------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Odyssey X86 Blue J4125</a> (affiliate link) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3ptnb8D" target="_blank" rel="nofollow noopener sponsored">Minisforum GK41</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/35E79BC" target="_blank" rel="nofollow noopener sponsored">Beelink GK55</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3psFlHi" target="_blank" rel="nofollow noopener sponsored">Intel NUC</a> (affiliate link) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. Requires extra parts. |
| <a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">BMAX B2 Plus</a> (affiliate link) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
| <a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Atomic Pi</a> (affiliate link) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (64bit)</a> (affiliate link) | 10-15ms | USB | Can handle a small number of cameras. |
## Google Coral TPU
@@ -41,3 +42,25 @@ The USB version is compatible with the widest variety of hardware and does not r
The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai
A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed.
### What does Frigate use the CPU for and what does it use the Coral for? (ELI5 Version)
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
CPU Usage: I am a CPU, Mendel is a Google Coral
My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it.
Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score!
_What happens when I increase the resolution of my camera?_
However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby.
Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it.
### Do hwaccel args help if I am using a Coral?
YES! The Coral does not help with decoding video streams.
Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://blog.video.ibm.com/streaming-video-tips/keyframes-interframe-video-compression/). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work.

View File

@@ -20,6 +20,6 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
## Screenshots
![Media Browser](/img/media_browser.png)
![Media Browser](/img/media_browser-min.png)
![Notification](/img/notification.png)
![Notification](/img/notification-min.png)

View File

@@ -13,17 +13,78 @@ Frigate is a Docker container that can be run on any Docker host including as a
### Operating System
Frigate runs best with docker installed on bare metal debian-based distributions. For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices.
Frigate runs best with docker installed on bare metal debian-based distributions. For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices, but [not in all circumstances](https://github.com/blakeblackshear/frigate/discussions/1837).
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.
:::caution
Note that Frigate does not currently support limiting recordings based on available disk space automatically. If using recordings, you must specify retention settings for a number of days that will fit within the available disk space of your drive or Frigate will crash.
:::
- `/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
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
The default shm-size of 64m is fine for setups with 2 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size.
You can calculate the necessary shm-size for each camera with the following formula:
You can calculate the necessary shm-size for each camera with the following formula using the resolution specified for detect:
```
(width * height * 1.5 * 9 + 270480)/1048576 = <shm size in mb>
@@ -35,7 +96,7 @@ The shm size cannot be set per container for Home Assistant Addons. You must set
By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options).
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with [this](https://amzn.to/2XTEqp7) (affiliate link).
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with <a href="https://amzn.to/3a2mH0P" target="_blank" rel="nofollow noopener sponsored">this</a> (affiliate link).
## Docker
@@ -63,6 +124,7 @@ services:
shm_size: "64mb" # update for your cameras based on calculation above
devices:
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
- /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
volumes:
- /etc/localtime:/etc/localtime:ro
@@ -102,7 +164,7 @@ docker run -d \
:::caution
Due to limitations in Home Assistant Operating System, Frigate cannot utilize external storage for recordings or snapshots.
Due to limitations in Home Assistant Operating System, utilizing external storage for recordings or snapshots requires [modifying udev rules manually](https://community.home-assistant.io/t/solved-mount-usb-drive-in-hassio-to-be-used-on-the-media-folder-with-udev-customization/258406/46).
:::
@@ -122,6 +184,15 @@ HassOS users can install via the addon repository.
6. Start the addon container
7. (not for proxy addon) If you are using hardware acceleration for ffmpeg, you may need to disable "Protection mode"
There are several versions of the addon available:
| Addon Version | Description |
| ------------------------------ | ---------------------------------------------------------- |
| Frigate NVR | Current release with protection mode on |
| Frigate NVR (Full Access) | Current release with the option to disable protection mode |
| Frigate NVR Beta | Beta release with protection mode on |
| Frigate NVR Beta (Full Access) | Beta release with the option to disable protection mode |
## Home Assistant Supervised
:::tip

View File

@@ -24,16 +24,6 @@ Accepts the following query string parameters:
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/api/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/api/back?fps=10` or both with `?fps=10&h=1000`.
### `GET /api/<camera_name>/<object_name>/best.jpg[?h=300&crop=1&quality=70]`
The best snapshot for any object type. It is a full resolution image by default.
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
- `quality=70`: sets the jpeg encoding quality (0-100)
### `GET /api/<camera_name>/latest.jpg[?h=300]`
The most recent frame that frigate has finished processing. It is a full resolution image by default.
@@ -120,7 +110,8 @@ Sample response:
"service": {
/* Uptime in seconds */
"uptime": 10,
"version": "0.8.0-8883709",
"version": "0.10.1-8883709",
"latest_version": "0.10.1",
/* Storage data in MB for important locations */
"storage": {
"/media/frigate/clips": {
@@ -188,10 +179,37 @@ Returns data for a single event.
Permanently deletes the event along with any clips/snapshots.
### `POST /api/events/<id>/retain`
Sets retain to true for the event id.
### `POST /api/events/<id>/plus`
Submits the snapshot of the event to Frigate+ for labeling.
### `DELETE /api/events/<id>/retain`
Sets retain to false for the event id (event may be deleted quickly after removing).
### `POST /api/events/<id>/sub_label`
Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition.
Sub labels must be 20 characters or shorter.
```json
{
"subLabel": "some_string"
}
```
### `GET /api/events/<id>/thumbnail.jpg`
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
### `GET /api/<camera_name>/<label>/thumbnail.jpg`
Returns the thumbnail from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
### `GET /api/events/<id>/clip.mp4`
Returns the clip for the event id. Works after the event has ended.
@@ -210,6 +228,10 @@ Accepts the following query string parameters, but they are only applied when an
| `crop` | int | Crop the snapshot to the (0 or 1) |
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
### `GET /api/<camera_name>/<label>/snapshot.jpg`
Returns the snapshot image from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
### `GET /clips/<camera>-<id>.jpg`
JPG snapshot for the given camera and event id.
@@ -229,3 +251,16 @@ HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed i
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
### `GET /api/<camera_name>/recordings/summary`
Hourly summary of recordings data for a camera.
### `GET /api/<camera_name>/recordings`
Get recording segment details for the given timestamp range.
| param | Type | Description |
| -------- | ---- | ------------------------------------- |
| `after` | int | Unix timestamp for beginning of range |
| `before` | int | Unix timestamp for end of range |

View File

@@ -45,11 +45,14 @@ that card.
## Configuration
When configuring the integration, you will be asked for the following parameters:
When configuring the integration, you will be asked for the `URL` of your frigate instance which is the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be one of the following depending on which addon version you are using. Note that if you are using the Proxy Addon, you do NOT point the integration at the proxy URL. Just enter the URL used to access frigate directly from your network.
| Variable | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| URL | The `URL` of your frigate instance, the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). Live streams required port 1935, see [RTMP streams](#streams) |
| Addon Version | URL |
| ------------------------------ | -------------------------------------- |
| Frigate NVR | `http://ccab4aaf-frigate:5000` |
| Frigate NVR (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
| Frigate NVR Beta | `http://ccab4aaf-frigate-beta:5000` |
| Frigate NVR Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
<a name="options"></a>

View File

@@ -18,10 +18,12 @@ Causes frigate to exit. Docker should be configured to automatically restart the
### `frigate/<camera_name>/<object_name>`
Publishes the count of objects for the camera for use as a sensor in Home Assistant.
`all` can be used as the object_name for the count of all objects for the camera.
### `frigate/<zone_name>/<object_name>`
Publishes the count of objects for the zone for use as a sensor in Home Assistant.
`all` can be used as the object_name for the count of all objects for the zone.
### `frigate/<camera_name>/<object_name>/snapshot`
@@ -50,12 +52,16 @@ Message published for each changed event. The first message is published when th
"score": 0.7890625,
"box": [424, 500, 536, 712],
"area": 23744,
"ratio": 2.113207,
"region": [264, 450, 667, 853],
"current_zones": ["driveway"],
"entered_zones": ["yard", "driveway"],
"thumbnail": null,
"has_snapshot": false,
"has_clip": false
"has_clip": false,
"stationary": false, // whether or not the object is considered stationary
"motionless_count": 0, // number of frames the object has been motionless
"position_changes": 2 // number of times the object has moved from a stationary position
},
"after": {
"id": "1607123955.475377-mxklsc",
@@ -70,12 +76,16 @@ Message published for each changed event. The first message is published when th
"score": 0.87890625,
"box": [432, 496, 544, 854],
"area": 40096,
"ratio": 1.251397,
"region": [218, 440, 693, 915],
"current_zones": ["yard", "driveway"],
"entered_zones": ["yard", "driveway"],
"thumbnail": null,
"has_snapshot": false,
"has_clip": false
"has_clip": false,
"stationary": false, // whether or not the object is considered stationary
"motionless_count": 0, // number of frames the object has been motionless
"position_changes": 2 // number of times the object has changed position
}
}
```
@@ -107,3 +117,42 @@ Topic to turn snapshots for a camera on and off. Expected values are `ON` and `O
### `frigate/<camera_name>/snapshots/state`
Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion/set`
Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`.
NOTE: Turning off motion detection will fail if detection is not disabled.
### `frigate/<camera_name>/motion`
Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`.
NOTE: After motion is initially detected, `ON` will be set until no motion has
been detected for `mqtt_off_delay` seconds (30 by default).
### `frigate/<camera_name>/motion/state`
Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/improve_contrast/set`
Topic to turn improve_contrast for a camera on and off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/improve_contrast/state`
Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion_threshold/set`
Topic to adjust motion threshold for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_threshold/state`
Topic with current motion threshold for a camera. Published value is an integer.
### `frigate/<camera_name>/motion_contour_area/set`
Topic to adjust motion contour area for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_contour_area/state`
Topic with current motion contour area for a camera. Published value is an integer.

View File

@@ -29,6 +29,16 @@ module.exports = {
label: 'Docs',
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',
label: 'GitHub',

14982
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,13 +12,13 @@
"clear": "docusaurus clear"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-beta.ff31de0ff",
"@docusaurus/preset-classic": "^2.0.0-beta.ff31de0ff",
"@mdx-js/react": "^1.6.21",
"@docusaurus/core": "^2.0.0-beta.20",
"@docusaurus/preset-classic": "^2.0.0-beta.20",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"raw-loader": "^4.0.2",
"react": "^16.8.4",
"react-dom": "^16.8.4"
"react": "^16.14.0",
"react-dom": "^16.14.0"
},
"browserslist": {
"production": [
@@ -31,5 +31,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^16.14.0"
}
}

View File

@@ -1,34 +1,35 @@
module.exports = {
docs: {
Frigate: [
'index',
'hardware',
'installation',
],
Frigate: ["index", "hardware", "installation"],
Guides: [
'guides/camera_setup',
'guides/getting_started',
'guides/false_positives',
'guides/ha_notifications',
'guides/stationary_objects',
"guides/camera_setup",
"guides/getting_started",
"guides/false_positives",
"guides/ha_notifications",
"guides/stationary_objects",
],
Configuration: [
'configuration/index',
'configuration/detectors',
'configuration/cameras',
'configuration/masks',
'configuration/record',
'configuration/snapshots',
'configuration/objects',
'configuration/rtmp',
'configuration/zones',
'configuration/advanced',
'configuration/hardware_acceleration',
'configuration/nvdec',
'configuration/camera_specific',
"configuration/index",
"configuration/detectors",
"configuration/cameras",
"configuration/masks",
"configuration/record",
"configuration/snapshots",
"configuration/objects",
"configuration/rtmp",
"configuration/zones",
"configuration/birdseye",
"configuration/stationary_objects",
"configuration/advanced",
"configuration/hardware_acceleration",
"configuration/camera_specific",
],
Integrations: ['integrations/home-assistant', 'integrations/api', 'integrations/mqtt'],
Troubleshooting: ['faqs'],
Development: ['contributing'],
Integrations: [
"integrations/home-assistant",
"integrations/api",
"integrations/mqtt",
],
Troubleshooting: ["faqs"],
Development: ["contributing"],
},
};

BIN
docs/static/img/driveway_zones-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/static/img/media_browser-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/static/img/notification-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
docs/static/img/reolink-settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/static/img/resolutions-min.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,14 +1,13 @@
import faulthandler
from flask import cli
faulthandler.enable()
import sys
import threading
threading.current_thread().name = "frigate"
from frigate.app import FrigateApp
cli = sys.modules["flask.cli"]
cli.show_server_banner = lambda *x: None
if __name__ == "__main__":

View File

@@ -1,17 +1,23 @@
import json
import logging
import multiprocessing as mp
from multiprocessing.queues import Queue
from multiprocessing.synchronize import Event
from multiprocessing.context import Process
import os
import signal
import sys
import threading
from logging.handlers import QueueHandler
from typing import Dict, List
from typing import Optional
from types import FrameType
import traceback
import yaml
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError
from frigate.config import DetectorTypeEnum, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
@@ -23,32 +29,33 @@ from frigate.models import Event, Recordings
from frigate.mqtt import MqttSocketRelay, create_mqtt_client
from frigate.object_processing import TrackedObjectProcessor
from frigate.output import output_frames
from frigate.plus import PlusApi
from frigate.record import RecordingCleanup, RecordingMaintainer
from frigate.stats import StatsEmitter, stats_init
from frigate.version import VERSION
from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog
from frigate.types import CameraMetricsTypes
logger = logging.getLogger(__name__)
class FrigateApp:
def __init__(self):
self.stop_event = mp.Event()
self.base_config: FrigateConfig = None
self.config: FrigateConfig = None
self.detection_queue = mp.Queue()
self.detectors: Dict[str, EdgeTPUProcess] = {}
self.detection_out_events: Dict[str, mp.Event] = {}
self.detection_shms: List[mp.shared_memory.SharedMemory] = []
self.log_queue = mp.Queue()
self.camera_metrics = {}
def __init__(self) -> None:
self.stop_event: Event = mp.Event()
self.detection_queue: Queue = mp.Queue()
self.detectors: dict[str, EdgeTPUProcess] = {}
self.detection_out_events: dict[str, Event] = {}
self.detection_shms: list[mp.shared_memory.SharedMemory] = []
self.log_queue: Queue = mp.Queue()
self.plus_api = PlusApi()
self.camera_metrics: dict[str, CameraMetricsTypes] = {}
def set_environment_vars(self):
def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items():
os.environ[key] = value
def ensure_dirs(self):
def ensure_dirs(self) -> None:
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
if not os.path.exists(d) and not os.path.islink(d):
logger.info(f"Creating directory: {d}")
@@ -56,7 +63,7 @@ class FrigateApp:
else:
logger.debug(f"Skipping directory: {d}")
def init_logger(self):
def init_logger(self) -> None:
self.log_process = mp.Process(
target=log_process, args=(self.log_queue,), name="log_process"
)
@@ -64,8 +71,14 @@ class FrigateApp:
self.log_process.start()
root_configurer(self.log_queue)
def init_config(self):
def init_config(self) -> None:
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml
config_file_yaml = config_file.replace(".yml", ".yaml")
if os.path.isfile(config_file_yaml):
config_file = config_file_yaml
user_config = FrigateConfig.parse_file(config_file)
self.config = user_config.runtime_config
@@ -78,37 +91,26 @@ class FrigateApp:
"detection_enabled": mp.Value(
"i", self.config.cameras[camera_name].detect.enabled
),
"motion_enabled": mp.Value("i", True),
"improve_contrast_enabled": mp.Value(
"i", self.config.cameras[camera_name].motion.improve_contrast
),
"motion_threshold": mp.Value(
"i", self.config.cameras[camera_name].motion.threshold
),
"motion_contour_area": mp.Value(
"i", self.config.cameras[camera_name].motion.contour_area
),
"detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0),
"read_start": mp.Value("d", 0.0),
"ffmpeg_pid": mp.Value("i", 0),
"frame_queue": mp.Queue(maxsize=2),
"capture_process": None,
"process": None,
}
def check_config(self):
for name, camera in self.config.cameras.items():
assigned_roles = list(
set([r for i in camera.ffmpeg.inputs for r in i.roles])
)
if not camera.record.enabled and "record" in assigned_roles:
logger.warning(
f"Camera {name} has record assigned to an input, but record is not enabled."
)
elif camera.record.enabled and not "record" in assigned_roles:
logger.warning(
f"Camera {name} has record enabled, but record is not assigned to an input."
)
if not camera.rtmp.enabled and "rtmp" in assigned_roles:
logger.warning(
f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled."
)
elif camera.rtmp.enabled and not "rtmp" in assigned_roles:
logger.warning(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
def set_log_levels(self):
def set_log_levels(self) -> None:
logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items():
logging.getLogger(log).setLevel(level.value.upper())
@@ -116,18 +118,23 @@ class FrigateApp:
if not "werkzeug" in self.config.logger.logs:
logging.getLogger("werkzeug").setLevel("ERROR")
def init_queues(self):
def init_queues(self) -> None:
# Queues for clip processing
self.event_queue = mp.Queue()
self.event_processed_queue = mp.Queue()
self.video_output_queue = mp.Queue(maxsize=len(self.config.cameras.keys()) * 2)
# Queue for cameras to push tracked objects to
self.detected_frames_queue = mp.Queue(
self.event_queue: Queue = mp.Queue()
self.event_processed_queue: Queue = mp.Queue()
self.video_output_queue: Queue = mp.Queue(
maxsize=len(self.config.cameras.keys()) * 2
)
def init_database(self):
# Queue for cameras to push tracked objects to
self.detected_frames_queue: Queue = mp.Queue(
maxsize=len(self.config.cameras.keys()) * 2
)
# Queue for recordings info
self.recordings_info_queue: Queue = mp.Queue()
def init_database(self) -> None:
# Migrate DB location
old_db_path = os.path.join(CLIPS_DIR, "frigate.db")
if not os.path.isfile(self.config.database.path) and os.path.isfile(
@@ -149,27 +156,28 @@ class FrigateApp:
models = [Event, Recordings]
self.db.bind(models)
def init_stats(self):
def init_stats(self) -> None:
self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
def init_web_server(self):
def init_web_server(self) -> None:
self.flask_app = create_app(
self.config,
self.db,
self.stats_tracking,
self.detected_frames_processor,
self.plus_api,
)
def init_mqtt(self):
def init_mqtt(self) -> None:
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
def start_mqtt_relay(self):
def start_mqtt_relay(self) -> None:
self.mqtt_relay = MqttSocketRelay(
self.mqtt_client, self.config.mqtt.topic_prefix
)
self.mqtt_relay.start()
def start_detectors(self):
def start_detectors(self) -> None:
model_path = self.config.model.path
model_shape = (self.config.model.height, self.config.model.width)
for name in self.config.cameras.keys():
@@ -216,7 +224,7 @@ class FrigateApp:
detector.num_threads,
)
def start_detected_frames_processor(self):
def start_detected_frames_processor(self) -> None:
self.detected_frames_processor = TrackedObjectProcessor(
self.config,
self.mqtt_client,
@@ -225,11 +233,12 @@ class FrigateApp:
self.event_queue,
self.event_processed_queue,
self.video_output_queue,
self.recordings_info_queue,
self.stop_event,
)
self.detected_frames_processor.start()
def start_video_output_processor(self):
def start_video_output_processor(self) -> None:
output_processor = mp.Process(
target=output_frames,
name=f"output_processor",
@@ -243,7 +252,7 @@ class FrigateApp:
output_processor.start()
logger.info(f"Output process started: {output_processor.pid}")
def start_camera_processors(self):
def start_camera_processors(self) -> None:
model_shape = (self.config.model.height, self.config.model.width)
for name, config in self.config.cameras.items():
camera_process = mp.Process(
@@ -265,7 +274,7 @@ class FrigateApp:
camera_process.start()
logger.info(f"Camera processor started for {name}: {camera_process.pid}")
def start_camera_capture_processes(self):
def start_camera_capture_processes(self) -> None:
for name, config in self.config.cameras.items():
capture_process = mp.Process(
target=capture_camera,
@@ -277,7 +286,7 @@ class FrigateApp:
capture_process.start()
logger.info(f"Capture process started for {name}: {capture_process.pid}")
def start_event_processor(self):
def start_event_processor(self) -> None:
self.event_processor = EventProcessor(
self.config,
self.camera_metrics,
@@ -287,19 +296,21 @@ class FrigateApp:
)
self.event_processor.start()
def start_event_cleanup(self):
def start_event_cleanup(self) -> None:
self.event_cleanup = EventCleanup(self.config, self.stop_event)
self.event_cleanup.start()
def start_recording_maintainer(self):
self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event)
def start_recording_maintainer(self) -> None:
self.recording_maintainer = RecordingMaintainer(
self.config, self.recordings_info_queue, self.stop_event
)
self.recording_maintainer.start()
def start_recording_cleanup(self):
def start_recording_cleanup(self) -> None:
self.recording_cleanup = RecordingCleanup(self.config, self.stop_event)
self.recording_cleanup.start()
def start_stats_emitter(self):
def start_stats_emitter(self) -> None:
self.stats_emitter = StatsEmitter(
self.config,
self.stats_tracking,
@@ -309,23 +320,35 @@ class FrigateApp:
)
self.stats_emitter.start()
def start_watchdog(self):
def start_watchdog(self) -> None:
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
self.frigate_watchdog.start()
def start(self):
def start(self) -> None:
self.init_logger()
logger.info(f"Starting Frigate ({VERSION})")
try:
try:
self.init_config()
except Exception as e:
print(f"Error parsing config: {e}")
print("*************************************************************")
print("*************************************************************")
print("*** Your config file is not valid! ***")
print("*** Please check the docs at ***")
print("*** https://docs.frigate.video/configuration/index ***")
print("*************************************************************")
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
print(e)
print(traceback.format_exc())
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
self.log_process.terminate()
sys.exit(1)
self.set_environment_vars()
self.ensure_dirs()
self.check_config()
self.set_log_levels()
self.init_queues()
self.init_database()
@@ -350,7 +373,7 @@ class FrigateApp:
self.start_watchdog()
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
def receiveSignal(signalNumber, frame):
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
self.stop()
sys.exit()
@@ -363,7 +386,7 @@ class FrigateApp:
self.stop()
def stop(self):
def stop(self) -> None:
logger.info(f"Stopping...")
self.stop_event.set()

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -12,9 +12,8 @@ import yaml
from pydantic import BaseModel, Extra, Field, validator
from pydantic.fields import PrivateAttr
from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR
from frigate.edgetpu import load_labels
from frigate.util import create_mask, deep_merge
from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
from frigate.util import create_mask, deep_merge, load_labels
logger = logging.getLogger(__name__)
@@ -45,6 +44,10 @@ class DetectorConfig(FrigateBaseModel):
num_threads: int = Field(default=3, title="Number of detection threads")
class UIConfig(FrigateBaseModel):
use_experimental: bool = Field(default=False, title="Experimental UI")
class MqttConfig(FrigateBaseModel):
host: str = Field(title="MQTT Host")
port: int = Field(default=1883, title="MQTT Port")
@@ -65,15 +68,21 @@ class MqttConfig(FrigateBaseModel):
return v
class RetainModeEnum(str, Enum):
all = "all"
motion = "motion"
active_objects = "active_objects"
class RetainConfig(FrigateBaseModel):
default: float = Field(default=10, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
objects: Dict[str, float] = Field(
default_factory=dict, title="Object retention period."
)
class EventsConfig(FrigateBaseModel):
max_seconds: int = Field(default=300, title="Maximum event duration.")
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
required_zones: List[str] = Field(
@@ -88,9 +97,22 @@ class EventsConfig(FrigateBaseModel):
)
class RecordRetainConfig(FrigateBaseModel):
days: float = Field(default=0, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
class RecordConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable record on all cameras.")
retain_days: float = Field(default=0, title="Recording retention period in days.")
expire_interval: int = Field(
default=60,
title="Number of minutes to wait between cleanup runs.",
)
# deprecated - to be removed in a future version
retain_days: Optional[float] = Field(title="Recording retention period in days.")
retain: RecordRetainConfig = Field(
default_factory=RecordRetainConfig, title="Record retention settings."
)
events: EventsConfig = Field(
default_factory=EventsConfig, title="Event specific settings."
)
@@ -103,13 +125,18 @@ class MotionConfig(FrigateBaseModel):
ge=1,
le=255,
)
contour_area: Optional[int] = Field(title="Contour Area")
improve_contrast: bool = Field(default=False, title="Improve Contrast")
contour_area: Optional[int] = Field(default=30, title="Contour Area")
delta_alpha: float = Field(default=0.2, title="Delta Alpha")
frame_alpha: float = Field(default=0.2, title="Frame Alpha")
frame_height: Optional[int] = Field(title="Frame Height")
frame_height: Optional[int] = Field(default=50, title="Frame Height")
mask: Union[str, List[str]] = Field(
default="", title="Coordinates polygon for the motion mask."
)
mqtt_off_delay: int = Field(
default=30,
title="Delay for updating MQTT with no motion detected.",
)
class RuntimeMotionConfig(MotionConfig):
@@ -119,15 +146,6 @@ class RuntimeMotionConfig(MotionConfig):
def __init__(self, **config):
frame_shape = config.get("frame_shape", (1, 1))
if "frame_height" not in config:
config["frame_height"] = max(frame_shape[0] // 6, 180)
if "contour_area" not in config:
frame_width = frame_shape[1] * config["frame_height"] / frame_shape[0]
config["contour_area"] = (
config["frame_height"] * frame_width * 0.00173611111
)
mask = config.get("mask", "")
config["raw_mask"] = mask
@@ -152,6 +170,29 @@ class RuntimeMotionConfig(MotionConfig):
extra = Extra.ignore
class StationaryMaxFramesConfig(FrigateBaseModel):
default: Optional[int] = Field(title="Default max frames.", ge=1)
objects: Dict[str, int] = Field(
default_factory=dict, title="Object specific max frames."
)
class StationaryConfig(FrigateBaseModel):
interval: Optional[int] = Field(
default=0,
title="Frame interval for checking stationary objects.",
ge=0,
)
threshold: Optional[int] = Field(
title="Number of frames without a position change for an object to be considered stationary",
ge=1,
)
max_frames: StationaryMaxFramesConfig = Field(
default_factory=StationaryMaxFramesConfig,
title="Max frames for stationary objects.",
)
class DetectConfig(FrigateBaseModel):
height: int = Field(default=720, title="Height of the stream for the detect role.")
width: int = Field(default=1280, title="Width of the stream for the detect role.")
@@ -162,6 +203,10 @@ class DetectConfig(FrigateBaseModel):
max_disappeared: Optional[int] = Field(
title="Maximum number of frames the object can dissapear before detection ends."
)
stationary: StationaryConfig = Field(
default_factory=StationaryConfig,
title="Stationary objects config.",
)
class FilterConfig(FrigateBaseModel):
@@ -171,6 +216,14 @@ class FilterConfig(FrigateBaseModel):
max_area: int = Field(
default=24000000, title="Maximum area of bounding box for object to be counted."
)
min_ratio: float = Field(
default=0,
title="Minimum ratio of bounding box's width/height for object to be counted.",
)
max_ratio: float = Field(
default=24000000,
title="Maximum ratio of bounding box's width/height for object to be counted.",
)
threshold: float = Field(
default=0.7,
title="Average detection confidence threshold for object to be counted.",
@@ -277,6 +330,14 @@ class BirdseyeConfig(FrigateBaseModel):
)
# uses BaseModel because some global attributes are not available at the camera level
class BirdseyeCameraConfig(BaseModel):
enabled: bool = Field(default=True, title="Enable birdseye view for camera.")
mode: BirdseyeModeEnum = Field(
default=BirdseyeModeEnum.objects, title="Tracking mode for camera."
)
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
FFMPEG_INPUT_ARGS_DEFAULT = [
"-avoid_negative_ts",
@@ -285,7 +346,7 @@ FFMPEG_INPUT_ARGS_DEFAULT = [
"+genpts+discardcorrupt",
"-rtsp_transport",
"tcp",
"-stimeout",
"-timeout",
"5000000",
"-use_wallclock_as_timestamps",
"1",
@@ -460,8 +521,15 @@ class CameraLiveConfig(FrigateBaseModel):
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
class CameraUiConfig(FrigateBaseModel):
order: int = Field(default=0, title="Order of camera in UI.")
dashboard: bool = Field(
default=True, title="Show this camera in Frigate dashboard UI."
)
class CameraConfig(FrigateBaseModel):
name: Optional[str] = Field(title="Camera name.")
name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
best_image_timeout: int = Field(
default=60,
@@ -492,9 +560,16 @@ class CameraConfig(FrigateBaseModel):
detect: DetectConfig = Field(
default_factory=DetectConfig, title="Object detection configuration."
)
ui: CameraUiConfig = Field(
default_factory=CameraUiConfig, title="Camera UI Modifications."
)
birdseye: BirdseyeCameraConfig = Field(
default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
)
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration."
)
_ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()
def __init__(self, **config):
# Set zone colors
@@ -505,6 +580,10 @@ class CameraConfig(FrigateBaseModel):
for idx, (name, z) in enumerate(config["zones"].items())
}
# add roles to the input if there is only one
if len(config["ffmpeg"]["inputs"]) == 1:
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
super().__init__(**config)
@property
@@ -517,6 +596,11 @@ class CameraConfig(FrigateBaseModel):
@property
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
return self._ffmpeg_cmds
def create_ffmpeg_cmds(self):
if "_ffmpeg_cmds" in self:
return
ffmpeg_cmds = []
for ffmpeg_input in self.ffmpeg.inputs:
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
@@ -524,7 +608,7 @@ class CameraConfig(FrigateBaseModel):
continue
ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
return ffmpeg_cmds
self._ffmpeg_cmds = ffmpeg_cmds
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
ffmpeg_output_args = []
@@ -560,6 +644,7 @@ class CameraConfig(FrigateBaseModel):
if isinstance(self.ffmpeg.output_args.record, list)
else self.ffmpeg.output_args.record.split(" ")
)
ffmpeg_output_args = (
record_args
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
@@ -618,7 +703,7 @@ class ModelConfig(FrigateBaseModel):
return self._merged_labelmap
@property
def colormap(self) -> Dict[int, tuple[int, int, int]]:
def colormap(self) -> Dict[int, Tuple[int, int, int]]:
return self._colormap
def __init__(self, **config):
@@ -661,6 +746,7 @@ class FrigateConfig(FrigateBaseModel):
environment_vars: Dict[str, str] = Field(
default_factory=dict, title="Frigate environment variables."
)
ui: UIConfig = Field(default_factory=UIConfig, title="UI configuration.")
model: ModelConfig = Field(
default_factory=ModelConfig, title="Detection model configuration."
)
@@ -716,6 +802,7 @@ class FrigateConfig(FrigateBaseModel):
# Global config to propegate down to camera level
global_config = config.dict(
include={
"birdseye": ...,
"record": ...,
"snapshots": ...,
"live": ...,
@@ -740,6 +827,11 @@ class FrigateConfig(FrigateBaseModel):
if camera_config.detect.max_disappeared is None:
camera_config.detect.max_disappeared = max_disappeared
# Default stationary_threshold configuration
stationary_threshold = camera_config.detect.fps * 10
if camera_config.detect.stationary.threshold is None:
camera_config.detect.stationary.threshold = stationary_threshold
# FFMPEG input substitution
for input in camera_config.ffmpeg.inputs:
input.path = input.path.format(**FRIGATE_ENV_VARS)
@@ -787,6 +879,44 @@ class FrigateConfig(FrigateBaseModel):
**camera_config.motion.dict(exclude_unset=True),
)
# check runtime config
assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
)
if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError(
f"Camera {name} has record enabled, but record is not assigned to an input."
)
if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
raise ValueError(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
# backwards compatibility for retain_days
if not camera_config.record.retain_days is None:
logger.warning(
"The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
)
if camera_config.record.retain.days == 0:
camera_config.record.retain.days = camera_config.record.retain_days
# warning if the higher level record mode is potentially more restrictive than the events
rank_map = {
RetainModeEnum.all: 0,
RetainModeEnum.motion: 1,
RetainModeEnum.active_objects: 2,
}
if (
camera_config.record.retain.days != 0
and rank_map[camera_config.record.retain.mode]
> rank_map[camera_config.record.events.retain.mode]
):
logger.warning(
f"{name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
)
# generage the ffmpeg commands
camera_config.create_ffmpeg_cmds()
config.cameras[name] = camera_config
return config
@@ -804,7 +934,7 @@ class FrigateConfig(FrigateBaseModel):
with open(config_file) as f:
raw_config = f.read()
if config_file.endswith(".yml"):
if config_file.endswith(YAML_EXT):
config = yaml.safe_load(raw_config)
elif config_file.endswith(".json"):
config = json.loads(raw_config)

View File

@@ -2,3 +2,6 @@ BASE_DIR = "/media/frigate"
CLIPS_DIR = f"{BASE_DIR}/clips"
RECORD_DIR = f"{BASE_DIR}/recordings"
CACHE_DIR = "/tmp/cache"
YAML_EXT = (".yaml", ".yml")
PLUS_ENV_VAR = "PLUS_API_KEY"
PLUS_API_HOST = "https://api.frigate.video"

View File

@@ -6,38 +6,17 @@ import queue
import signal
import threading
from abc import ABC, abstractmethod
from typing import Dict
import numpy as np
import tflite_runtime.interpreter as tflite
from setproctitle import setproctitle
from tflite_runtime.interpreter import load_delegate
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels
logger = logging.getLogger(__name__)
def load_labels(path, encoding="utf-8"):
"""Loads labels from file (with or without index numbers).
Args:
path: path to label file.
encoding: label file encoding.
Returns:
Dictionary mapping indices to labels.
"""
with open(path, "r", encoding=encoding) as f:
lines = f.readlines()
if not lines:
return {}
if lines[0].split(" ", maxsplit=1)[0].isdigit():
pairs = [line.split(" ", maxsplit=1) for line in lines]
return {int(index): label.strip() for index, label in pairs}
else:
return {index: line.strip() for index, line in enumerate(lines)}
class ObjectDetector(ABC):
@abstractmethod
def detect(self, tensor_input, threshold=0.4):
@@ -130,7 +109,7 @@ class LocalObjectDetector(ObjectDetector):
def run_detector(
name: str,
detection_queue: mp.Queue,
out_events: Dict[str, mp.Event],
out_events: dict[str, mp.Event],
avg_speed,
start,
model_path,

View File

@@ -15,6 +15,24 @@ from frigate.models import Event
logger = logging.getLogger(__name__)
def should_insert_db(prev_event, current_event):
"""If current event has new clip or snapshot."""
return (not prev_event["has_clip"] and not prev_event["has_snapshot"]) and (
current_event["has_clip"] or current_event["has_snapshot"]
)
def should_update_db(prev_event, current_event):
"""If current_event has updated fields and (clip or snapshot)."""
return (current_event["has_clip"] or current_event["has_snapshot"]) and (
prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["thumbnail"] != current_event["thumbnail"]
or prev_event["has_clip"] != current_event["has_clip"]
or prev_event["has_snapshot"] != current_event["has_snapshot"]
)
class EventProcessor(threading.Thread):
def __init__(
self, config, camera_processes, event_queue, event_processed_queue, stop_event
@@ -30,6 +48,11 @@ class EventProcessor(threading.Thread):
self.stop_event = stop_event
def run(self):
# set an end_time on events without an end_time on startup
Event.update(end_time=Event.start_time + 30).where(
Event.end_time == None
).execute()
while not self.stop_event.is_set():
try:
event_type, camera, event_data = self.event_queue.get(timeout=10)
@@ -38,15 +61,59 @@ class EventProcessor(threading.Thread):
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
event_config: EventsConfig = self.config.cameras[camera].record.events
if event_type == "start":
self.events_in_process[event_data["id"]] = event_data
if event_type == "end":
event_config: EventsConfig = self.config.cameras[camera].record.events
elif event_type == "update" and should_insert_db(
self.events_in_process[event_data["id"]], event_data
):
self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly
Event.insert(
id=event_data["id"],
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
end_time=None,
top_score=event_data["top_score"],
false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"],
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).execute()
elif event_type == "update" and should_update_db(
self.events_in_process[event_data["id"]], event_data
):
self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly
Event.update(
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
end_time=None,
top_score=event_data["top_score"],
false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"],
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
ratio=event_data["ratio"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).where(Event.id == event_data["id"]).execute()
elif event_type == "end":
if event_data["has_clip"] or event_data["has_snapshot"]:
Event.create(
id=event_data["id"],
# Full update for valid end of event
Event.update(
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
@@ -58,13 +125,24 @@ class EventProcessor(threading.Thread):
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
ratio=event_data["ratio"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
)
).where(Event.id == event_data["id"]).execute()
else:
# Event ended after clip & snapshot disabled,
# only end time should be updated.
Event.update(
end_time=event_data["end_time"] + event_config.post_capture
).where(Event.id == event_data["id"]).execute()
del self.events_in_process[event_data["id"]]
self.event_processed_queue.put((event_data["id"], camera))
# set an end_time on events without an end_time before exiting
Event.update(end_time=datetime.datetime.now().timestamp()).where(
Event.end_time == None
).execute()
logger.info(f"Exiting event processor...")
@@ -105,6 +183,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
# delete the media from disk
for event in expired_events:
@@ -124,6 +203,7 @@ class EventCleanup(threading.Thread):
Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
update_query.execute()
@@ -150,6 +230,7 @@ class EventCleanup(threading.Thread):
Event.camera == name,
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
# delete the grabbed clips from disk
for event in expired_events:
@@ -168,6 +249,7 @@ class EventCleanup(threading.Thread):
Event.camera == name,
Event.start_time < expire_after,
Event.label == l.label,
Event.retain_indefinitely == False,
)
update_query.execute()
@@ -192,12 +274,12 @@ class EventCleanup(threading.Thread):
for event in duplicate_events:
logger.debug(f"Removing duplicate: {event.id}")
media_name = f"{event.camera}-{event.id}"
if event.has_snapshot:
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media_path.unlink(missing_ok=True)
if event.has_clip:
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media_path.unlink(missing_ok=True)
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media_path.unlink(missing_ok=True)
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
media_path.unlink(missing_ok=True)
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media_path.unlink(missing_ok=True)
(
Event.delete()

View File

@@ -1,18 +1,15 @@
import base64
from collections import OrderedDict
from datetime import datetime, timedelta
import json
import glob
import copy
import logging
import os
import re
import subprocess as sp
import time
from functools import reduce
from pathlib import Path
import cv2
from flask.helpers import send_file
import numpy as np
from flask import (
@@ -25,13 +22,12 @@ from flask import (
request,
)
from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value
from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR, RECORD_DIR
from frigate.const import CLIPS_DIR
from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot
from frigate.util import calculate_region
from frigate.version import VERSION
logger = logging.getLogger(__name__)
@@ -44,6 +40,7 @@ def create_app(
database: SqliteDatabase,
stats_tracking,
detected_frames_processor,
plus_api,
):
app = Flask(__name__)
@@ -60,6 +57,7 @@ def create_app(
app.frigate_config = frigate_config
app.stats_tracking = stats_tracking
app.detected_frames_processor = detected_frames_processor
app.plus_api = plus_api
app.register_blueprint(bp)
@@ -119,35 +117,186 @@ def event(id):
return "Event not found", 404
@bp.route("/events/<id>/retain", methods=("POST",))
def set_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
event.retain_indefinitely = True
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
)
@bp.route("/events/<id>/plus", methods=("POST",))
def send_to_plus(id):
if not current_app.plus_api.is_active():
message = "PLUS_API_KEY environment variable is not set"
logger.error(message)
return make_response(
jsonify(
{
"success": False,
"message": message,
}
),
400,
)
try:
event = Event.get(Event.id == id)
except DoesNotExist:
message = f"Event {id} not found"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 404)
if event.plus_id:
message = "Already submitted to plus"
logger.error(message)
return make_response(jsonify({"success": False, "message": message}), 400)
# load clean.png
try:
filename = f"{event.camera}-{event.id}-clean.png"
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
except Exception:
logger.error(f"Unable to load clean png for event: {event.id}")
return make_response(
jsonify(
{"success": False, "message": "Unable to load clean png for event"}
),
400,
)
try:
plus_id = current_app.plus_api.upload_image(image, event.camera)
except Exception as ex:
logger.exception(ex)
return make_response(
jsonify({"success": False, "message": str(ex)}),
400,
)
# store image id in the database
event.plus_id = plus_id
event.save()
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
@bp.route("/events/<id>/retain", methods=("DELETE",))
def delete_retain(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
event.retain_indefinitely = False
event.save()
return make_response(
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
)
@bp.route("/events/<id>/sub_label", methods=("POST",))
def set_sub_label(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
if request.json:
new_sub_label = request.json.get("subLabel")
else:
new_sub_label = None
if new_sub_label and len(new_sub_label) > 20:
return make_response(
jsonify(
{
"success": False,
"message": new_sub_label
+ " exceeds the 20 character limit for sub_label",
}
),
400,
)
event.sub_label = new_sub_label
event.save()
return make_response(
jsonify(
{
"success": True,
"message": "Event " + id + " sub label set to " + new_sub_label,
}
),
200,
)
@bp.route("/sub_labels")
def get_sub_labels():
try:
events = Event.select(Event.sub_label).distinct()
except Exception as e:
return jsonify(
{"success": False, "message": f"Failed to get sub_labels: {e}"}, "404"
)
sub_labels = [e.sub_label for e in events]
if None in sub_labels:
sub_labels.remove(None)
return jsonify(sub_labels)
@bp.route("/events/<id>", methods=("DELETE",))
def delete_event(id):
try:
event = Event.get(Event.id == id)
except DoesNotExist:
return make_response(
jsonify({"success": False, "message": "Event" + id + " not found"}), 404
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
)
media_name = f"{event.camera}-{event.id}"
if event.has_snapshot:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media.unlink(missing_ok=True)
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
media.unlink(missing_ok=True)
if event.has_clip:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media.unlink(missing_ok=True)
event.delete_instance()
return make_response(
jsonify({"success": True, "message": "Event" + id + " deleted"}), 200
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
)
@bp.route("/events/<id>/thumbnail.jpg")
def event_thumbnail(id):
def event_thumbnail(id, max_cache_age=2592000):
format = request.args.get("format", "ios")
thumbnail_bytes = None
event_complete = False
try:
event = Event.get(Event.id == id)
if not event.end_time is None:
event_complete = True
thumbnail_bytes = base64.b64decode(event.thumbnail)
except DoesNotExist:
# see if the object is currently being tracked
@@ -181,16 +330,55 @@ def event_thumbnail(id):
thumbnail_bytes = jpg.tobytes()
response = make_response(thumbnail_bytes)
response.headers["Content-Type"] = "image/jpg"
response.headers["Content-Type"] = "image/jpeg"
if event_complete:
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
else:
response.headers["Cache-Control"] = "no-store"
return response
@bp.route("/<camera_name>/<label>/best.jpg")
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
def label_thumbnail(camera_name, label):
if label == "any":
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
else:
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.label == label)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
try:
event = event_query.get()
return event_thumbnail(event.id, 60)
except DoesNotExist:
frame = np.zeros((175, 175, 3), np.uint8)
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response
@bp.route("/events/<id>/snapshot.jpg")
def event_snapshot(id):
download = request.args.get("download", type=bool)
event_complete = False
jpg_bytes = None
try:
event = Event.get(Event.id == id)
event = Event.get(Event.id == id, Event.end_time != None)
event_complete = True
if not event.has_snapshot:
return "Snapshot not available", 404
# read snapshot from disk
@@ -222,7 +410,11 @@ def event_snapshot(id):
return "Event not found", 404
response = make_response(jpg_bytes)
response.headers["Content-Type"] = "image/jpg"
response.headers["Content-Type"] = "image/jpeg"
if event_complete:
response.headers["Cache-Control"] = "private, max-age=31536000"
else:
response.headers["Cache-Control"] = "no-store"
if download:
response.headers[
"Content-Disposition"
@@ -230,6 +422,36 @@ def event_snapshot(id):
return response
@bp.route("/<camera_name>/<label>/snapshot.jpg")
def label_snapshot(camera_name, label):
if label == "any":
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
else:
event_query = (
Event.select()
.where(Event.camera == camera_name)
.where(Event.label == label)
.where(Event.has_snapshot == True)
.order_by(Event.start_time.desc())
)
try:
event = event_query.get()
return event_snapshot(event.id)
except DoesNotExist:
frame = np.zeros((720, 1280, 3), np.uint8)
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
return response
@bp.route("/events/<id>/clip.mp4")
def event_clip(id):
download = request.args.get("download", type=bool)
@@ -246,7 +468,10 @@ def event_clip(id):
clip_path = os.path.join(CLIPS_DIR, file_name)
if not os.path.isfile(clip_path):
return recording_clip(event.camera, event.start_time, event.end_time)
end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time
)
return recording_clip(event.camera, event.start_time, end_ts)
response = make_response()
response.headers["Content-Description"] = "File Transfer"
@@ -265,9 +490,10 @@ def event_clip(id):
@bp.route("/events")
def events():
limit = request.args.get("limit", 100)
camera = request.args.get("camera")
label = request.args.get("label")
zone = request.args.get("zone")
camera = request.args.get("camera", "all")
label = request.args.get("label", "all")
sub_label = request.args.get("sub_label", "all")
zone = request.args.get("zone", "all")
after = request.args.get("after", type=float)
before = request.args.get("before", type=float)
has_clip = request.args.get("has_clip", type=int)
@@ -277,20 +503,38 @@ def events():
clauses = []
excluded_fields = []
if camera:
selected_columns = [
Event.id,
Event.camera,
Event.label,
Event.zones,
Event.start_time,
Event.end_time,
Event.has_clip,
Event.has_snapshot,
Event.plus_id,
Event.retain_indefinitely,
Event.sub_label,
Event.top_score,
]
if camera != "all":
clauses.append((Event.camera == camera))
if label:
if label != "all":
clauses.append((Event.label == label))
if zone:
if sub_label != "all":
clauses.append((Event.sub_label == sub_label))
if zone != "all":
clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
if after:
clauses.append((Event.start_time >= after))
clauses.append((Event.start_time > after))
if before:
clauses.append((Event.start_time <= before))
clauses.append((Event.start_time < before))
if not has_clip is None:
clauses.append((Event.has_clip == has_clip))
@@ -300,12 +544,14 @@ def events():
if not include_thumbnails:
excluded_fields.append(Event.thumbnail)
else:
selected_columns.append(Event.thumbnail)
if len(clauses) == 0:
clauses.append((True))
events = (
Event.select()
Event.select(*selected_columns)
.where(reduce(operator.and_, clauses))
.order_by(Event.start_time.desc())
.limit(limit)
@@ -321,10 +567,12 @@ def config():
# add in the ffmpeg_cmds
for camera_name, camera in current_app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name]
camera_dict["ffmpeg_cmds"] = camera.ffmpeg_cmds
camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"])
config["plus"] = {"enabled": current_app.plus_api.is_active()}
return jsonify(config)
@@ -346,41 +594,6 @@ def stats():
return jsonify(stats)
@bp.route("/<camera_name>/<label>/best.jpg")
def best(camera_name, label):
if camera_name in current_app.frigate_config.cameras:
best_object = current_app.detected_frames_processor.get_best(camera_name, label)
best_frame = best_object.get("frame")
if best_frame is None:
best_frame = np.zeros((720, 1280, 3), np.uint8)
else:
best_frame = cv2.cvtColor(best_frame, cv2.COLOR_YUV2BGR_I420)
crop = bool(request.args.get("crop", 0, type=int))
if crop:
box = best_object.get("box", (0, 0, 300, 300))
region = calculate_region(
best_frame.shape, box[0], box[1], box[2], box[3], 1.1
)
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
height = int(request.args.get("h", str(best_frame.shape[0])))
width = int(height * best_frame.shape[1] / best_frame.shape[0])
resize_quality = request.args.get("quality", default=70, type=int)
best_frame = cv2.resize(
best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA
)
ret, jpg = cv2.imencode(
".jpg", best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
)
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpg"
return response
else:
return "Camera named {} not found".format(camera_name), 404
@bp.route("/<camera_name>")
def mjpeg_feed(camera_name):
fps = int(request.args.get("fps", "3"))
@@ -437,123 +650,107 @@ def latest_frame(camera_name):
".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
)
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpg"
response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response
else:
return "Camera named {} not found".format(camera_name), 404
# return hourly summary for recordings of camera
@bp.route("/<camera_name>/recordings/summary")
def recordings_summary(camera_name):
recording_groups = (
Recordings.select(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
).alias("hour"),
fn.SUM(Recordings.duration).alias("duration"),
fn.SUM(Recordings.motion).alias("motion"),
fn.SUM(Recordings.objects).alias("objects"),
)
.where(Recordings.camera == camera_name)
.group_by(
fn.strftime(
"%Y-%m-%d %H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
)
)
.order_by(
fn.strftime(
"%Y-%m-%d H",
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
).desc()
)
)
event_groups = (
Event.select(
fn.strftime(
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
).alias("hour"),
fn.COUNT(Event.id).alias("count"),
)
.where(Event.camera == camera_name, Event.has_clip)
.group_by(
fn.strftime(
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
),
)
.objects()
)
event_map = {g.hour: g.count for g in event_groups}
days = {}
for recording_group in recording_groups.objects():
parts = recording_group.hour.split()
hour = parts[1]
day = parts[0]
events_count = event_map.get(recording_group.hour, 0)
hour_data = {
"hour": hour,
"events": events_count,
"motion": recording_group.motion,
"objects": recording_group.objects,
"duration": round(recording_group.duration),
}
if day not in days:
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
else:
days[day]["events"] += events_count
days[day]["hours"].append(hour_data)
return jsonify(list(days.values()))
# return hour of recordings data for camera
@bp.route("/<camera_name>/recordings")
def recordings(camera_name):
dates = OrderedDict()
after = request.args.get(
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
)
before = request.args.get("before", type=float, default=datetime.now().timestamp())
# Retrieve all recordings for this camera
recordings = (
Recordings.select()
.where(Recordings.camera == camera_name)
.order_by(Recordings.start_time.asc())
)
last_end = 0
recording: Recordings
for recording in recordings:
date = datetime.fromtimestamp(recording.start_time)
key = date.strftime("%Y-%m-%d")
hour = date.strftime("%H")
# Create Day Record
if key not in dates:
dates[key] = OrderedDict()
# Create Hour Record
if hour not in dates[key]:
dates[key][hour] = {"delay": {}, "events": []}
# Check for delay
the_hour = datetime.strptime(f"{key} {hour}", "%Y-%m-%d %H").timestamp()
# diff current recording start time and the greater of the previous end time or top of the hour
diff = recording.start_time - max(last_end, the_hour)
# Determine seconds into recording
seconds = 0
if datetime.fromtimestamp(last_end).strftime("%H") == hour:
seconds = int(last_end - the_hour)
# Determine the delay
delay = min(int(diff), 3600 - seconds)
if delay > 1:
# Add an offset for any delay greater than a second
dates[key][hour]["delay"][seconds] = delay
last_end = recording.end_time
# Packing intervals to return all events with same label and overlapping times as one row.
# See: https://blogs.solidq.com/en/sqlserver/packing-intervals/
events = Event.raw(
"""WITH C1 AS
(
SELECT id, label, camera, top_score, start_time AS ts, +1 AS type, 1 AS sub
FROM event
WHERE camera = ?
UNION ALL
SELECT id, label, camera, top_score, end_time + 15 AS ts, -1 AS type, 0 AS sub
FROM event
WHERE camera = ?
),
C2 AS
(
SELECT C1.*,
SUM(type) OVER(PARTITION BY label ORDER BY ts, type DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) - sub AS cnt
FROM C1
),
C3 AS
(
SELECT id, label, camera, top_score, ts,
(ROW_NUMBER() OVER(PARTITION BY label ORDER BY ts) - 1) / 2 + 1
AS grpnum
FROM C2
WHERE cnt = 0
Recordings.select(
Recordings.id,
Recordings.start_time,
Recordings.end_time,
Recordings.motion,
Recordings.objects,
)
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
FROM C3
GROUP BY label, grpnum
ORDER BY start_time;""",
camera_name,
camera_name,
.where(
Recordings.camera == camera_name,
Recordings.end_time >= after,
Recordings.start_time <= before,
)
.order_by(Recordings.start_time)
)
event: Event
for event in events:
date = datetime.fromtimestamp(event.start_time)
key = date.strftime("%Y-%m-%d")
hour = date.strftime("%H")
if key in dates and hour in dates[key]:
dates[key][hour]["events"].append(
model_to_dict(
event,
exclude=[
Event.false_positive,
Event.zones,
Event.thumbnail,
Event.has_clip,
Event.has_snapshot,
],
)
)
return jsonify(
[
{
"date": date,
"events": sum([len(value["events"]) for value in hours.values()]),
"recordings": [
{"hour": hour, "delay": value["delay"], "events": value["events"]}
for hour, value in hours.items()
],
}
for date, hours in dates.items()
]
)
return jsonify([e for e in recordings.dicts()])
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
@@ -596,7 +793,7 @@ def recording_clip(camera, start_ts, end_ts):
"-safe",
"0",
"-i",
"-",
"/dev/stdin",
"-c",
"copy",
"-movflags",
@@ -657,10 +854,15 @@ def vod_ts(camera, start_ts, end_ts):
# Determine if we need to end the last clip early
if recording.end_time > end_ts:
duration -= int((recording.end_time - end_ts) * 1000)
clips.append(clip)
durations.append(duration)
if duration > 0:
clips.append(clip)
durations.append(duration)
else:
logger.warning(f"Recording clip is missing or empty: {recording.path}")
if not clips:
logger.error("No recordings found for the requested time range")
return "No recordings found.", 404
hour_ago = datetime.now() - timedelta(hours=1)
@@ -689,15 +891,28 @@ def vod_event(id):
try:
event: Event = Event.get(Event.id == id)
except DoesNotExist:
logger.error(f"Event not found: {id}")
return "Event not found.", 404
if not event.has_clip:
return "Clip not available", 404
logger.error(f"Event does not have recordings: {id}")
return "Recordings not available", 404
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
if not os.path.isfile(clip_path):
return vod_ts(event.camera, event.start_time, event.end_time)
end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time
)
vod_response = vod_ts(event.camera, event.start_time, end_ts)
# If the recordings are not found, set has_clip to false
if (
type(vod_response) == tuple
and len(vod_response) == 2
and vod_response[1] == 404
):
Event.update(has_clip=False).where(Event.id == id).execute()
return vod_response
duration = int((event.end_time - event.start_time) * 1000)
return jsonify(

View File

@@ -4,13 +4,14 @@ import threading
import os
import signal
import queue
import multiprocessing as mp
from multiprocessing.queues import Queue
from logging import handlers
from setproctitle import setproctitle
from typing import Deque
from collections import deque
def listener_configurer():
def listener_configurer() -> None:
root = logging.getLogger()
console_handler = logging.StreamHandler()
formatter = logging.Formatter(
@@ -21,14 +22,14 @@ def listener_configurer():
root.setLevel(logging.INFO)
def root_configurer(queue):
def root_configurer(queue: Queue) -> None:
h = handlers.QueueHandler(queue)
root = logging.getLogger()
root.addHandler(h)
root.setLevel(logging.INFO)
def log_process(log_queue):
def log_process(log_queue: Queue) -> None:
threading.current_thread().name = f"logger"
setproctitle("frigate.logger")
listener_configurer()
@@ -43,34 +44,32 @@ def log_process(log_queue):
# based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread):
def __init__(self, log_name, level):
"""Setup the object with a logger and a loglevel
and start the thread
"""
def __init__(self, log_name: str):
"""Setup the object with a logger and start the thread"""
threading.Thread.__init__(self)
self.daemon = False
self.logger = logging.getLogger(log_name)
self.level = level
self.deque = deque(maxlen=100)
self.level = logging.ERROR
self.deque: Deque[str] = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
def fileno(self):
def fileno(self) -> int:
"""Return the write file descriptor of the pipe"""
return self.fdWrite
def run(self):
def run(self) -> None:
"""Run the thread, logging everything."""
for line in iter(self.pipeReader.readline, ""):
self.deque.append(line.strip("\n"))
self.pipeReader.close()
def dump(self):
def dump(self) -> None:
while len(self.deque) > 0:
self.logger.log(self.level, self.deque.popleft())
def close(self):
def close(self) -> None:
"""Close the write end of the pipe."""
os.close(self.fdWrite)

View File

@@ -1,11 +1,20 @@
from numpy import unique
from peewee import *
from playhouse.sqlite_ext import *
from peewee import (
Model,
CharField,
DateTimeField,
FloatField,
BooleanField,
TextField,
IntegerField,
)
from playhouse.sqlite_ext import JSONField
class Event(Model):
class Event(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30)
label = CharField(index=True, max_length=20)
sub_label = CharField(max_length=20, null=True)
camera = CharField(index=True, max_length=20)
start_time = DateTimeField()
end_time = DateTimeField()
@@ -18,12 +27,17 @@ class Event(Model):
region = JSONField()
box = JSONField()
area = IntegerField()
retain_indefinitely = BooleanField(default=False)
ratio = FloatField(default=1.0)
plus_id = CharField(max_length=30)
class Recordings(Model):
class Recordings(Model): # type: ignore[misc]
id = CharField(null=False, primary_key=True, max_length=30)
camera = CharField(index=True, max_length=20)
path = CharField(unique=True)
start_time = DateTimeField()
end_time = DateTimeField()
duration = FloatField()
motion = IntegerField(null=True)
objects = IntegerField(null=True)

View File

@@ -5,7 +5,14 @@ from frigate.config import MotionConfig
class MotionDetector:
def __init__(self, frame_shape, config: MotionConfig):
def __init__(
self,
frame_shape,
config: MotionConfig,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
):
self.config = config
self.frame_shape = frame_shape
self.resize_factor = frame_shape[0] / config.frame_height
@@ -23,6 +30,10 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR,
)
self.mask = np.where(resized_mask == [0])
self.save_images = False
self.improve_contrast = improve_contrast_enabled
self.threshold = motion_threshold
self.contour_area = motion_contour_area
def detect(self, frame):
motion_boxes = []
@@ -36,10 +47,16 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR,
)
# TODO: can I improve the contrast of the grayscale image here?
# convert to grayscale
# resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY)
# Improve contrast
if self.improve_contrast.value:
minval = np.percentile(resized_frame, 4)
maxval = np.percentile(resized_frame, 96)
# don't adjust if the image is a single color
if minval < maxval:
resized_frame = np.clip(resized_frame, minval, maxval)
resized_frame = (
((resized_frame - minval) / (maxval - minval)) * 255
).astype(np.uint8)
# mask frame
resized_frame[self.mask] = [255]
@@ -49,6 +66,8 @@ class MotionDetector:
if self.frame_counter < 30:
self.frame_counter += 1
else:
if self.save_images:
self.frame_counter += 1
# compare to average
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
@@ -58,9 +77,8 @@ class MotionDetector:
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
# compute the threshold image for the current frame
# TODO: threshold
current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY
frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY
)[1]
# black out everything in the avg_delta where there isnt motion in the current frame
@@ -70,20 +88,22 @@ class MotionDetector:
# then look for deltas above the threshold, but only in areas where there is a delta
# in the current frame. this prevents deltas from previous frames from being included
thresh = cv2.threshold(
avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY
avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY
)[1]
# dilate the thresholded image to fill in holes, then find contours
# on thresholded image
thresh = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
thresh_dilated = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(
thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
cnts = imutils.grab_contours(cnts)
# loop over the contours
for c in cnts:
# if the contour is big enough, count it as motion
contour_area = cv2.contourArea(c)
if contour_area > self.config.contour_area:
if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c)
motion_boxes.append(
(
@@ -94,6 +114,34 @@ class MotionDetector:
)
)
if self.save_images:
thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR)
# print("--------")
# print(self.frame_counter)
for c in cnts:
contour_area = cv2.contourArea(c)
if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(
thresh_dilated,
(x, y),
(x + w, y + h),
(0, 0, 255),
2,
)
# print("--------")
image_row_1 = cv2.hconcat(
[
cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR),
cv2.cvtColor(avg_delta_image, cv2.COLOR_GRAY2BGR),
]
)
image_row_2 = cv2.hconcat(
[cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), thresh_dilated]
)
combined_image = cv2.vconcat([image_row_1, image_row_2])
cv2.imwrite(f"motion/motion-{self.frame_counter}.jpg", combined_image)
if len(motion_boxes) > 0:
self.motion_frame_count += 1
if self.motion_frame_count >= 10:

View File

@@ -78,6 +78,12 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
logger.info(f"Turning on detection for {camera_name} via mqtt")
camera_metrics[camera_name]["detection_enabled"].value = True
detect_settings.enabled = True
if not camera_metrics[camera_name]["motion_enabled"].value:
logger.info(
f"Turning on motion for {camera_name} due to detection being enabled."
)
camera_metrics[camera_name]["motion_enabled"].value = True
elif payload == "OFF":
if camera_metrics[camera_name]["detection_enabled"].value:
logger.info(f"Turning off detection for {camera_name} via mqtt")
@@ -89,6 +95,102 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_command(client, userdata, message):
payload = message.payload.decode()
logger.debug(f"on_motion_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
if payload == "ON":
if not camera_metrics[camera_name]["motion_enabled"].value:
logger.info(f"Turning on motion for {camera_name} via mqtt")
camera_metrics[camera_name]["motion_enabled"].value = True
elif payload == "OFF":
if camera_metrics[camera_name]["detection_enabled"].value:
logger.error(
f"Turning off motion is not allowed when detection is enabled."
)
return
if camera_metrics[camera_name]["motion_enabled"].value:
logger.info(f"Turning off motion for {camera_name} via mqtt")
camera_metrics[camera_name]["motion_enabled"].value = False
else:
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_improve_contrast_command(client, userdata, message):
payload = message.payload.decode()
logger.debug(f"on_improve_contrast_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
if payload == "ON":
if not camera_metrics[camera_name]["improve_contrast_enabled"].value:
logger.info(f"Turning on improve contrast for {camera_name} via mqtt")
camera_metrics[camera_name]["improve_contrast_enabled"].value = True
motion_settings.improve_contrast = True
elif payload == "OFF":
if camera_metrics[camera_name]["improve_contrast_enabled"].value:
logger.info(f"Turning off improve contrast for {camera_name} via mqtt")
camera_metrics[camera_name]["improve_contrast_enabled"].value = False
motion_settings.improve_contrast = False
else:
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_threshold_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
camera_metrics[camera_name]["motion_threshold"].value = payload
motion_settings.threshold = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_contour_area_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
)
camera_metrics[camera_name]["motion_contour_area"].value = payload
motion_settings.contour_area = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_restart_command(client, userdata, message):
restart_frigate()
@@ -96,9 +198,13 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
threading.current_thread().name = "mqtt"
if rc != 0:
if rc == 3:
logger.error("Unable to connect to MQTT server: MQTT Server unavailable")
logger.error(
"Unable to connect to MQTT server: MQTT Server unavailable"
)
elif rc == 4:
logger.error("Unable to connect to MQTT server: MQTT Bad username or password")
logger.error(
"Unable to connect to MQTT server: MQTT Bad username or password"
)
elif rc == 5:
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
else:
@@ -107,7 +213,7 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
+ str(rc)
)
logger.info("MQTT connected")
logger.debug("MQTT connected")
client.subscribe(f"{mqtt_config.topic_prefix}/#")
client.publish(mqtt_config.topic_prefix + "/available", "online", retain=True)
@@ -128,6 +234,21 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion/set", on_motion_command
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set",
on_improve_contrast_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/set",
on_motion_threshold_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
on_motion_contour_area_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/restart", on_restart_command
@@ -173,6 +294,31 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
"ON" if config.cameras[name].detect.enabled else "OFF",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion/state",
"ON",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/state",
"ON" if config.cameras[name].motion.improve_contrast else "OFF",
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/state",
config.cameras[name].motion.threshold,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/state",
config.cameras[name].motion.contour_area,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion",
"OFF",
retain=False,
)
return client

60
frigate/mypy.ini Normal file
View File

@@ -0,0 +1,60 @@
[mypy]
python_version = 3.9
show_error_codes = true
follow_imports = normal
ignore_missing_imports = true
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = ignore-without-code
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
no_implicit_reexport = true
[mypy-frigate.*]
ignore_errors = true
[mypy-frigate.__main__]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.app]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.const]
ignore_errors = false
[mypy-frigate.log]
ignore_errors = false
[mypy-frigate.models]
ignore_errors = false
[mypy-frigate.plus]
ignore_errors = false
[mypy-frigate.stats]
ignore_errors = false
[mypy-frigate.types]
ignore_errors = false
[mypy-frigate.version]
ignore_errors = false
[mypy-frigate.watchdog]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.zeroconf]
ignore_errors = false

View File

@@ -1,24 +1,19 @@
import base64
import copy
import datetime
import hashlib
import itertools
import json
import logging
import os
import queue
import threading
import time
from collections import Counter, defaultdict
from statistics import mean, median
from typing import Callable, Dict
from statistics import median
from typing import Callable
import cv2
import numpy as np
from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
from frigate.edgetpu import load_labels
from frigate.const import CLIPS_DIR
from frigate.util import (
SharedMemoryFrameManager,
calculate_region,
@@ -71,7 +66,7 @@ class TrackedObject:
self.camera_config = camera_config
self.frame_cache = frame_cache
self.current_zones = []
self.entered_zones = set()
self.entered_zones = []
self.false_positive = True
self.has_clip = False
self.has_snapshot = False
@@ -101,14 +96,13 @@ class TrackedObject:
return median(scores)
def update(self, current_frame_time, obj_data):
significant_update = False
zone_change = False
self.obj_data.update(obj_data)
thumb_update = False
significant_change = False
# if the object is not in the current frame, add a 0.0 to the score history
if self.obj_data["frame_time"] != current_frame_time:
if obj_data["frame_time"] != current_frame_time:
self.score_history.append(0.0)
else:
self.score_history.append(self.obj_data["score"])
self.score_history.append(obj_data["score"])
# only keep the last 10 scores
if len(self.score_history) > 10:
self.score_history = self.score_history[-10:]
@@ -122,24 +116,24 @@ class TrackedObject:
if not self.false_positive:
# determine if this frame is a better thumbnail
if self.thumbnail_data is None or is_better_thumbnail(
self.thumbnail_data, self.obj_data, self.camera_config.frame_shape
self.thumbnail_data, obj_data, self.camera_config.frame_shape
):
self.thumbnail_data = {
"frame_time": self.obj_data["frame_time"],
"box": self.obj_data["box"],
"area": self.obj_data["area"],
"region": self.obj_data["region"],
"score": self.obj_data["score"],
"frame_time": obj_data["frame_time"],
"box": obj_data["box"],
"area": obj_data["area"],
"region": obj_data["region"],
"score": obj_data["score"],
}
significant_update = True
thumb_update = True
# check zones
current_zones = []
bottom_center = (self.obj_data["centroid"][0], self.obj_data["box"][3])
bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
# check each zone
for name, zone in self.camera_config.zones.items():
# if the zone is not for this object type, skip
if len(zone.objects) > 0 and not self.obj_data["label"] in zone.objects:
if len(zone.objects) > 0 and not obj_data["label"] in zone.objects:
continue
contour = zone.contour
# check if the object is in the zone
@@ -147,14 +141,32 @@ class TrackedObject:
# if the object passed the filters once, dont apply again
if name in self.current_zones or not zone_filtered(self, zone.filters):
current_zones.append(name)
self.entered_zones.add(name)
if name not in self.entered_zones:
self.entered_zones.append(name)
# if the zones changed, signal an update
if not self.false_positive and set(self.current_zones) != set(current_zones):
zone_change = True
if not self.false_positive:
# if the zones changed, signal an update
if set(self.current_zones) != set(current_zones):
significant_change = True
# if the position changed, signal an update
if self.obj_data["position_changes"] != obj_data["position_changes"]:
significant_change = True
# if the motionless_count reaches the stationary threshold
if (
self.obj_data["motionless_count"]
== self.camera_config.detect.stationary.threshold
):
significant_change = True
# update at least once per minute
if self.obj_data["frame_time"] - self.previous["frame_time"] > 60:
significant_change = True
self.obj_data.update(obj_data)
self.current_zones = current_zones
return (significant_update, zone_change)
return (thumb_update, significant_change)
def to_dict(self, include_thumbnail: bool = False):
snapshot_time = (
@@ -175,9 +187,14 @@ class TrackedObject:
"score": self.obj_data["score"],
"box": self.obj_data["box"],
"area": self.obj_data["area"],
"ratio": self.obj_data["ratio"],
"region": self.obj_data["region"],
"stationary": self.obj_data["motionless_count"]
> self.camera_config.detect.stationary.threshold,
"motionless_count": self.obj_data["motionless_count"],
"position_changes": self.obj_data["position_changes"],
"current_zones": self.current_zones.copy(),
"entered_zones": list(self.entered_zones).copy(),
"entered_zones": self.entered_zones.copy(),
"has_clip": self.has_clip,
"has_snapshot": self.has_snapshot,
}
@@ -262,8 +279,15 @@ class TrackedObject:
if crop:
box = self.thumbnail_data["box"]
box_size = 300
region = calculate_region(
best_frame.shape, box[0], box[1], box[2], box[3], 1.1
best_frame.shape,
box[0],
box[1],
box[2],
box[3],
box_size,
multiplier=1.1,
)
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
@@ -313,6 +337,14 @@ def zone_filtered(obj: TrackedObject, object_config):
if obj_settings.threshold > obj.computed_score:
return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > obj.obj_data["ratio"]:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < obj.obj_data["ratio"]:
return True
return False
@@ -325,9 +357,9 @@ class CameraState:
self.config = config
self.camera_config = config.cameras[name]
self.frame_manager = frame_manager
self.best_objects: Dict[str, TrackedObject] = {}
self.best_objects: dict[str, TrackedObject] = {}
self.object_counts = defaultdict(int)
self.tracked_objects: Dict[str, TrackedObject] = {}
self.tracked_objects: dict[str, TrackedObject] = {}
self.frame_cache = {}
self.zone_objects = defaultdict(list)
self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8)
@@ -424,7 +456,7 @@ class CameraState:
def finished(self, obj_id):
del self.tracked_objects[obj_id]
def on(self, event_type: str, callback: Callable[[Dict], None]):
def on(self, event_type: str, callback: Callable[[dict], None]):
self.callbacks[event_type].append(callback)
def update(self, frame_time, current_detections, motion_boxes, regions):
@@ -456,11 +488,11 @@ class CameraState:
for id in updated_ids:
updated_obj = tracked_objects[id]
significant_update, zone_change = updated_obj.update(
thumb_update, significant_update = updated_obj.update(
frame_time, current_detections[id]
)
if significant_update:
if thumb_update:
# ensure this frame is stored in the cache
if (
updated_obj.thumbnail_data["frame_time"] == frame_time
@@ -470,13 +502,13 @@ class CameraState:
updated_obj.last_updated = frame_time
# if it has been more than 5 seconds since the last publish
# if it has been more than 5 seconds since the last thumb update
# and the last update is greater than the last publish or
# the object has changed zones
# the object has changed significantly
if (
frame_time - updated_obj.last_published > 5
and updated_obj.last_updated > updated_obj.last_published
) or zone_change:
) or significant_update:
# call event handlers
for c in self.callbacks["update"]:
c(self.name, updated_obj, frame_time)
@@ -526,13 +558,24 @@ class CameraState:
if not obj.false_positive
)
# keep track of all labels detected for this camera
total_label_count = 0
# report on detected objects
for obj_name, count in obj_counter.items():
total_label_count += count
if count != self.object_counts[obj_name]:
self.object_counts[obj_name] = count
for c in self.callbacks["object_status"]:
c(self.name, obj_name, count)
# publish for all labels detected for this camera
if total_label_count != self.object_counts.get("all"):
self.object_counts["all"] = total_label_count
for c in self.callbacks["object_status"]:
c(self.name, "all", total_label_count)
# expire any objects that are >0 and no longer detected
expired_objects = [
obj_name
@@ -540,6 +583,10 @@ class CameraState:
if count > 0 and obj_name not in obj_counter
]
for obj_name in expired_objects:
# Ignore the artificial all label
if obj_name == "all":
continue
self.object_counts[obj_name] = 0
for c in self.callbacks["object_status"]:
c(self.name, obj_name, 0)
@@ -584,6 +631,7 @@ class TrackedObjectProcessor(threading.Thread):
event_queue,
event_processed_queue,
video_output_queue,
recordings_info_queue,
stop_event,
):
threading.Thread.__init__(self)
@@ -595,14 +643,18 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue = event_queue
self.event_processed_queue = event_processed_queue
self.video_output_queue = video_output_queue
self.recordings_info_queue = recordings_info_queue
self.stop_event = stop_event
self.camera_states: Dict[str, CameraState] = {}
self.camera_states: dict[str, CameraState] = {}
self.frame_manager = SharedMemoryFrameManager()
self.last_motion_detected: dict[str, float] = {}
def start(camera, obj: TrackedObject, current_frame_time):
self.event_queue.put(("start", camera, obj.to_dict()))
def update(camera, obj: TrackedObject, current_frame_time):
obj.has_snapshot = self.should_save_snapshot(camera, obj)
obj.has_clip = self.should_retain_recording(camera, obj)
after = obj.to_dict()
message = {
"before": obj.previous,
@@ -613,6 +665,9 @@ class TrackedObjectProcessor(threading.Thread):
f"{self.topic_prefix}/events", json.dumps(message), retain=False
)
obj.previous = after
self.event_queue.put(
("update", camera, obj.to_dict(include_thumbnail=True))
)
def end(camera, obj: TrackedObject, current_frame_time):
# populate has_snapshot
@@ -722,9 +777,13 @@ class TrackedObjectProcessor(threading.Thread):
if not snapshot_config.enabled:
return False
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# if there are required zones and there is no overlap
required_zones = snapshot_config.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
logger.debug(
f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones"
)
@@ -742,6 +801,10 @@ class TrackedObjectProcessor(threading.Thread):
if not record_config.enabled:
return False
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# If there are required zones and there is no overlap
required_zones = record_config.events.required_zones
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
@@ -763,9 +826,13 @@ class TrackedObjectProcessor(threading.Thread):
return True
def should_mqtt_snapshot(self, camera, obj: TrackedObject):
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].mqtt.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
logger.debug(
f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones"
)
@@ -773,6 +840,32 @@ class TrackedObjectProcessor(threading.Thread):
return True
def update_mqtt_motion(self, camera, frame_time, motion_boxes):
# publish if motion is currently being detected
if motion_boxes:
# only send ON if motion isn't already active
if self.last_motion_detected.get(camera, 0) == 0:
self.client.publish(
f"{self.topic_prefix}/{camera}/motion",
"ON",
retain=False,
)
# always updated latest motion
self.last_motion_detected[camera] = frame_time
elif self.last_motion_detected.get(camera, 0) > 0:
mqtt_delay = self.config.cameras[camera].motion.mqtt_off_delay
# If no motion, make sure the off_delay has passed
if frame_time - self.last_motion_detected.get(camera, 0) >= mqtt_delay:
self.client.publish(
f"{self.topic_prefix}/{camera}/motion",
"OFF",
retain=False,
)
# reset the last_motion so redundant `off` commands aren't sent
self.last_motion_detected[camera] = 0
def get_best(self, camera, label):
# TODO: need a lock here
camera_state = self.camera_states[camera]
@@ -808,11 +901,28 @@ class TrackedObjectProcessor(threading.Thread):
frame_time, current_tracked_objects, motion_boxes, regions
)
self.update_mqtt_motion(camera, frame_time, motion_boxes)
tracked_objects = [
o.to_dict() for o in camera_state.tracked_objects.values()
]
self.video_output_queue.put(
(
camera,
frame_time,
current_tracked_objects,
tracked_objects,
motion_boxes,
regions,
)
)
# send info on this frame to the recordings maintainer
self.recordings_info_queue.put(
(
camera,
frame_time,
tracked_objects,
motion_boxes,
regions,
)
@@ -827,9 +937,14 @@ class TrackedObjectProcessor(threading.Thread):
for obj in camera_state.tracked_objects.values()
if zone in obj.current_zones and not obj.false_positive
)
total_label_count = 0
# update counts and publish status
for label in set(self.zone_data[zone].keys()) | set(obj_counter.keys()):
# Ignore the artificial all label
if label == "all":
continue
# if we have previously published a count for this zone/label
zone_label = self.zone_data[zone][label]
if camera in zone_label:
@@ -844,6 +959,10 @@ class TrackedObjectProcessor(threading.Thread):
new_count,
retain=False,
)
# Set the count for the /zone/all topic.
total_label_count += new_count
# if this is a new zone/label combo for this camera
else:
if label in obj_counter:
@@ -854,6 +973,31 @@ class TrackedObjectProcessor(threading.Thread):
retain=False,
)
# Set the count for the /zone/all topic.
total_label_count += obj_counter[label]
# if we have previously published a count for this zone all labels
zone_label = self.zone_data[zone]["all"]
if camera in zone_label:
current_count = sum(zone_label.values())
zone_label[camera] = total_label_count
new_count = sum(zone_label.values())
if new_count != current_count:
self.client.publish(
f"{self.topic_prefix}/{zone}/all",
new_count,
retain=False,
)
# if this is a new zone all label for this camera
else:
zone_label[camera] = total_label_count
self.client.publish(
f"{self.topic_prefix}/{zone}/all",
total_label_count,
retain=False,
)
# cleanup event finished queue
while not self.event_processed_queue.empty():
event_id, camera = self.event_processed_queue.get()

View File

@@ -13,31 +13,134 @@ import numpy as np
from scipy.spatial import distance as dist
from frigate.config import DetectConfig
from frigate.util import draw_box_with_label
from frigate.util import intersection_over_union
class ObjectTracker:
def __init__(self, config: DetectConfig):
self.tracked_objects = {}
self.disappeared = {}
self.positions = {}
self.max_disappeared = config.max_disappeared
self.detect_config = config
def register(self, index, obj):
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
id = f"{obj['frame_time']}-{rand_id}"
obj["id"] = id
obj["start_time"] = obj["frame_time"]
obj["motionless_count"] = 0
obj["position_changes"] = 0
self.tracked_objects[id] = obj
self.disappeared[id] = 0
self.positions[id] = {
"xmins": [],
"ymins": [],
"xmaxs": [],
"ymaxs": [],
"xmin": 0,
"ymin": 0,
"xmax": self.detect_config.width,
"ymax": self.detect_config.height,
}
def deregister(self, id):
del self.tracked_objects[id]
del self.disappeared[id]
# tracks the current position of the object based on the last N bounding boxes
# returns False if the object has moved outside its previous position
def update_position(self, id, box):
position = self.positions[id]
position_box = (
position["xmin"],
position["ymin"],
position["xmax"],
position["ymax"],
)
xmin, ymin, xmax, ymax = box
iou = intersection_over_union(position_box, box)
# if the iou drops below the threshold
# assume the object has moved to a new position and reset the computed box
if iou < 0.6:
self.positions[id] = {
"xmins": [xmin],
"ymins": [ymin],
"xmaxs": [xmax],
"ymaxs": [ymax],
"xmin": xmin,
"ymin": ymin,
"xmax": xmax,
"ymax": ymax,
}
return False
# if there are less than 10 entries for the position, add the bounding box
# and recompute the position box
if len(position["xmins"]) < 10:
position["xmins"].append(xmin)
position["ymins"].append(ymin)
position["xmaxs"].append(xmax)
position["ymaxs"].append(ymax)
# by using percentiles here, we hopefully remove outliers
position["xmin"] = np.percentile(position["xmins"], 15)
position["ymin"] = np.percentile(position["ymins"], 15)
position["xmax"] = np.percentile(position["xmaxs"], 85)
position["ymax"] = np.percentile(position["ymaxs"], 85)
return True
def is_expired(self, id):
obj = self.tracked_objects[id]
# get the max frames for this label type or the default
max_frames = self.detect_config.stationary.max_frames.objects.get(
obj["label"], self.detect_config.stationary.max_frames.default
)
# if there is no max_frames for this label type, continue
if max_frames is None:
return False
# if the object has exceeded the max_frames setting, deregister
if (
obj["motionless_count"] - self.detect_config.stationary.threshold
> max_frames
):
return True
return False
def update(self, id, new_obj):
self.disappeared[id] = 0
# update the motionless count if the object has not moved to a new position
if self.update_position(id, new_obj["box"]):
self.tracked_objects[id]["motionless_count"] += 1
if self.is_expired(id):
self.deregister(id)
return
else:
# register the first position change and then only increment if
# the object was previously stationary
if (
self.tracked_objects[id]["position_changes"] == 0
or self.tracked_objects[id]["motionless_count"]
>= self.detect_config.stationary.threshold
):
self.tracked_objects[id]["position_changes"] += 1
self.tracked_objects[id]["motionless_count"] = 0
self.tracked_objects[id].update(new_obj)
def update_frame_times(self, frame_time):
for id in list(self.tracked_objects.keys()):
self.tracked_objects[id]["frame_time"] = frame_time
self.tracked_objects[id]["motionless_count"] += 1
if self.is_expired(id):
self.deregister(id)
def match_and_update(self, frame_time, new_objects):
# group by name
new_object_groups = defaultdict(lambda: [])
@@ -48,7 +151,8 @@ class ObjectTracker:
"score": obj[1],
"box": obj[2],
"area": obj[3],
"region": obj[4],
"ratio": obj[4],
"region": obj[5],
"frame_time": frame_time,
}
)

View File

@@ -22,6 +22,7 @@ from ws4py.server.wsgiutils import WebSocketWSGIApplication
from ws4py.websocket import WebSocket
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.const import BASE_DIR
from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv_crop
logger = logging.getLogger(__name__)
@@ -104,12 +105,21 @@ class BirdsEyeFrameManager:
self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16
# find and copy the logo on the blank frame
logo_files = glob.glob("/opt/frigate/web/apple-touch-icon.*.png")
frigate_logo = None
if len(logo_files) > 0:
frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED)
if not frigate_logo is None:
transparent_layer = frigate_logo[:, :, 3]
birdseye_logo = None
custom_logo_files = glob.glob(f"{BASE_DIR}/custom.png")
if len(custom_logo_files) > 0:
birdseye_logo = cv2.imread(custom_logo_files[0], cv2.IMREAD_UNCHANGED)
if birdseye_logo is None:
logo_files = glob.glob("/opt/frigate/frigate/birdseye.png")
if len(logo_files) > 0:
birdseye_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED)
if not birdseye_logo is None:
transparent_layer = birdseye_logo[:, :, 3]
y_offset = height // 2 - transparent_layer.shape[0] // 2
x_offset = width // 2 - transparent_layer.shape[1] // 2
self.blank_frame[
@@ -180,17 +190,14 @@ class BirdsEyeFrameManager:
channel_dims,
)
def camera_active(self, object_box_count, motion_box_count):
if self.mode == BirdseyeModeEnum.continuous:
def camera_active(self, mode, object_box_count, motion_box_count):
if mode == BirdseyeModeEnum.continuous:
return True
if (
self.mode == BirdseyeModeEnum.motion
and object_box_count + motion_box_count > 0
):
if mode == BirdseyeModeEnum.motion and motion_box_count > 0:
return True
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0:
if mode == BirdseyeModeEnum.objects and object_box_count > 0:
return True
def update_frame(self):
@@ -304,10 +311,14 @@ class BirdsEyeFrameManager:
return True
def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
# don't process if birdseye is disabled for this camera
camera_config = self.config.cameras[camera].birdseye
if not camera_config.enabled:
return False
# update the last active frame for the camera
self.cameras[camera]["current_frame"] = frame_time
if self.camera_active(object_count, motion_count):
if self.camera_active(camera_config.mode, object_count, motion_count):
self.cameras[camera]["last_active_frame"] = frame_time
now = datetime.datetime.now().timestamp()
@@ -418,7 +429,7 @@ def output_frames(config: FrigateConfig, video_output_queue):
):
if birdseye_manager.update(
camera,
len(current_tracked_objects),
len([o for o in current_tracked_objects if not o["stationary"]]),
len(motion_boxes),
frame_time,
frame,

126
frigate/plus.py Normal file
View File

@@ -0,0 +1,126 @@
import datetime
import json
import logging
import os
import re
import requests
from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST
from requests.models import Response
import cv2
from numpy import ndarray
logger = logging.getLogger(__name__)
def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
if image.shape[1] >= image.shape[0]:
width = min(max_dim, image.shape[1])
height = int(width * image.shape[0] / image.shape[1])
else:
height = min(max_dim, image.shape[0])
width = int(height * image.shape[1] / image.shape[0])
original = cv2.resize(image, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, jpg = cv2.imencode(".jpg", original, [int(cv2.IMWRITE_JPEG_QUALITY), quality])
jpg_bytes = jpg.tobytes()
return jpg_bytes if type(jpg_bytes) is bytes else b""
class PlusApi:
def __init__(self) -> None:
self.host = PLUS_API_HOST
self.key = None
if PLUS_ENV_VAR in os.environ:
self.key = os.environ.get(PLUS_ENV_VAR)
# check for the addon options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
self.key = options.get("plus_api_key")
if self.key is not None and not re.match(
r"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}",
self.key,
):
logger.error("Plus API Key is not formatted correctly.")
self.key = None
self._is_active: bool = self.key is not None
self._token_data: dict = {}
def _refresh_token_if_needed(self) -> None:
if (
self._token_data.get("expires") is None
or self._token_data["expires"] - datetime.datetime.now().timestamp() < 60
):
if self.key is None:
raise Exception("Plus API not activated")
parts = self.key.split(":")
r = requests.get(f"{self.host}/v1/auth/token", auth=(parts[0], parts[1]))
if not r.ok:
raise Exception("Unable to refresh API token")
self._token_data = r.json()
def _get_authorization_header(self) -> dict:
self._refresh_token_if_needed()
return {"authorization": f"Bearer {self._token_data.get('accessToken')}"}
def _get(self, path: str) -> Response:
return requests.get(
f"{self.host}/v1/{path}", headers=self._get_authorization_header()
)
def _post(self, path: str, data: dict) -> Response:
return requests.post(
f"{self.host}/v1/{path}",
headers=self._get_authorization_header(),
json=data,
)
def is_active(self) -> bool:
return self._is_active
def upload_image(self, image: ndarray, camera: str) -> str:
r = self._get("image/signed_urls")
presigned_urls = r.json()
if not r.ok:
raise Exception("Unable to get signed urls")
# resize and submit original
files = {"file": get_jpg_bytes(image, 1920, 85)}
data = presigned_urls["original"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["original"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload original: {r.status_code} {r.text}")
raise Exception(r.text)
# resize and submit annotate
files = {"file": get_jpg_bytes(image, 640, 70)}
data = presigned_urls["annotate"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["annotate"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload annotate: {r.status_code} {r.text}")
raise Exception(r.text)
# resize and submit thumbnail
files = {"file": get_jpg_bytes(image, 200, 70)}
data = presigned_urls["thumbnail"]["fields"]
data["content-type"] = "image/jpeg"
r = requests.post(presigned_urls["thumbnail"]["url"], files=files, data=data)
if not r.ok:
logger.error(f"Failed to upload thumbnail: {r.status_code} {r.text}")
raise Exception(r.text)
# create image
r = self._post(
"image/create", {"id": presigned_urls["imageId"], "camera": camera}
)
if not r.ok:
raise Exception(r.text)
# return image id
return str(presigned_urls.get("imageId"))

View File

@@ -1,20 +1,24 @@
import datetime
import itertools
import logging
import multiprocessing as mp
import os
import queue
import random
import shutil
import string
import subprocess as sp
import threading
from collections import defaultdict
from pathlib import Path
import psutil
from peewee import JOIN, DoesNotExist
from frigate.config import FrigateConfig
from frigate.config import RetainModeEnum, FrigateConfig
from frigate.const import CACHE_DIR, RECORD_DIR
from frigate.models import Event, Recordings
from frigate.util import area
logger = logging.getLogger(__name__)
@@ -38,20 +42,27 @@ def remove_empty_directories(directory):
class RecordingMaintainer(threading.Thread):
def __init__(self, config: FrigateConfig, stop_event):
def __init__(
self, config: FrigateConfig, recordings_info_queue: mp.Queue, stop_event
):
threading.Thread.__init__(self)
self.name = "recording_maint"
self.config = config
self.recordings_info_queue = recordings_info_queue
self.stop_event = stop_event
self.recordings_info = defaultdict(list)
self.end_time_cache = {}
def move_files(self):
recordings = [
d
for d in os.listdir(CACHE_DIR)
if os.path.isfile(os.path.join(CACHE_DIR, d))
and d.endswith(".mp4")
and not d.startswith("clip_")
]
cache_files = sorted(
[
d
for d in os.listdir(CACHE_DIR)
if os.path.isfile(os.path.join(CACHE_DIR, d))
and d.endswith(".mp4")
and not d.startswith("clip_")
]
)
files_in_use = []
for process in psutil.process_iter():
@@ -66,7 +77,9 @@ class RecordingMaintainer(threading.Thread):
except:
continue
for f in recordings:
# group recordings by camera
grouped_recordings = defaultdict(list)
for f in cache_files:
# Skip files currently in use
if f in files_in_use:
continue
@@ -76,45 +89,199 @@ class RecordingMaintainer(threading.Thread):
camera, date = basename.rsplit("-", maxsplit=1)
start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
# Just delete files if recordings are turned off
if (
not camera in self.config.cameras
or not self.config.cameras[camera].record.enabled
):
Path(cache_path).unlink(missing_ok=True)
continue
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
else:
logger.info(f"bad file: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
directory = os.path.join(
RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera
grouped_recordings[camera].append(
{
"cache_path": cache_path,
"start_time": start_time,
}
)
if not os.path.exists(directory):
os.makedirs(directory)
# delete all cached files past the most recent 5
keep_count = 5
for camera in grouped_recordings.keys():
segment_count = len(grouped_recordings[camera])
if segment_count > keep_count:
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(
# f"Too many recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {segment_count}, discarding the rest..."
# )
to_remove = grouped_recordings[camera][:-keep_count]
for f in to_remove:
cache_path = f["cache_path"]
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(f"Discarding a recording segment: {cache_path}")
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
grouped_recordings[camera] = grouped_recordings[camera][-keep_count:]
file_name = f"{start_time.strftime('%M.%S.mp4')}"
file_path = os.path.join(directory, file_name)
for camera, recordings in grouped_recordings.items():
# clear out all the recording info for old frames
while (
len(self.recordings_info[camera]) > 0
and self.recordings_info[camera][0][0]
< recordings[0]["start_time"].timestamp()
):
self.recordings_info[camera].pop(0)
# get all events with the end time after the start of the oldest cache file
# or with end_time None
events: Event = (
Event.select()
.where(
Event.camera == camera,
(Event.end_time == None)
| (Event.end_time >= recordings[0]["start_time"].timestamp()),
Event.has_clip,
)
.order_by(Event.start_time)
)
for r in recordings:
cache_path = r["cache_path"]
start_time = r["start_time"]
# Just delete files if recordings are turned off
if (
not camera in self.config.cameras
or not self.config.cameras[camera].record.enabled
):
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
continue
if cache_path in self.end_time_cache:
end_time, duration = self.end_time_cache[cache_path]
else:
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
self.end_time_cache[cache_path] = (end_time, duration)
else:
logger.warning(f"Discarding a corrupt recording segment: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
# if cached file's start_time is earlier than the retain days for the camera
if start_time <= (
(
datetime.datetime.now()
- datetime.timedelta(
days=self.config.cameras[camera].record.retain.days
)
)
):
# if the cached segment overlaps with the events:
overlaps = False
for event in events:
# if the event starts in the future, stop checking events
# and remove this segment
if event.start_time > end_time.timestamp():
overlaps = False
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
break
# if the event is in progress or ends after the recording starts, keep it
# and stop looking at events
if (
event.end_time is None
or event.end_time >= start_time.timestamp()
):
overlaps = True
break
if overlaps:
record_mode = self.config.cameras[
camera
].record.events.retain.mode
# move from cache to recordings immediately
self.store_segment(
camera,
start_time,
end_time,
duration,
cache_path,
record_mode,
)
# else retain days includes this segment
else:
record_mode = self.config.cameras[camera].record.retain.mode
self.store_segment(
camera, start_time, end_time, duration, cache_path, record_mode
)
def segment_stats(self, camera, start_time, end_time):
active_count = 0
motion_count = 0
for frame in self.recordings_info[camera]:
# frame is after end time of segment
if frame[0] > end_time.timestamp():
break
# frame is before start time of segment
if frame[0] < start_time.timestamp():
continue
active_count += len(
[
o
for o in frame[1]
if not o["false_positive"] and o["motionless_count"] == 0
]
)
motion_count += sum([area(box) for box in frame[2]])
return (motion_count, active_count)
def store_segment(
self,
camera,
start_time,
end_time,
duration,
cache_path,
store_mode: RetainModeEnum,
):
motion_count, active_count = self.segment_stats(camera, start_time, end_time)
# check if the segment shouldn't be stored
if (store_mode == RetainModeEnum.motion and motion_count == 0) or (
store_mode == RetainModeEnum.active_objects and active_count == 0
):
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
return
directory = os.path.join(RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera)
if not os.path.exists(directory):
os.makedirs(directory)
file_name = f"{start_time.strftime('%M.%S.mp4')}"
file_path = os.path.join(directory, file_name)
try:
start_frame = datetime.datetime.now().timestamp()
# copy then delete is required when recordings are stored on some network drives
shutil.copyfile(cache_path, file_path)
logger.debug(
f"Copied {file_path} in {datetime.datetime.now().timestamp()-start_frame} seconds."
)
os.remove(cache_path)
rand_id = "".join(
@@ -127,12 +294,56 @@ class RecordingMaintainer(threading.Thread):
start_time=start_time.timestamp(),
end_time=end_time.timestamp(),
duration=duration,
motion=motion_count,
# TODO: update this to store list of active objects at some point
objects=active_count,
)
except Exception as e:
logger.error(f"Unable to store recording segment {cache_path}")
Path(cache_path).unlink(missing_ok=True)
logger.error(e)
# clear end_time cache
self.end_time_cache.pop(cache_path, None)
def run(self):
# Check for new files every 5 seconds
while not self.stop_event.wait(5):
self.move_files()
wait_time = 5
while not self.stop_event.wait(wait_time):
run_start = datetime.datetime.now().timestamp()
# empty the recordings info queue
while True:
try:
(
camera,
frame_time,
current_tracked_objects,
motion_boxes,
regions,
) = self.recordings_info_queue.get(False)
if self.config.cameras[camera].record.enabled:
self.recordings_info[camera].append(
(
frame_time,
current_tracked_objects,
motion_boxes,
regions,
)
)
except queue.Empty:
break
try:
self.move_files()
except Exception as e:
logger.error(
"Error occurred when attempting to maintain recording cache"
)
logger.error(e)
duration = datetime.datetime.now().timestamp() - run_start
wait_time = max(0, 5 - duration)
logger.info(f"Exiting recording maintenance...")
@@ -157,7 +368,7 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start deleted cameras.")
# Handle deleted cameras
expire_days = self.config.record.retain_days
expire_days = self.config.record.retain.days
expire_before = (
datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp()
@@ -178,16 +389,11 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start all cameras.")
for camera, config in self.config.cameras.items():
logger.debug(f"Start camera: {camera}.")
# When deleting recordings without events, we have to keep at LEAST the configured max clip duration
min_end = (
datetime.datetime.now()
- datetime.timedelta(seconds=config.record.events.max_seconds)
).timestamp()
expire_days = config.record.retain_days
expire_before = (
# Get the timestamp for cutoff of retained days
expire_days = config.record.retain.days
expire_date = (
datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp()
expire_date = min(min_end, expire_before)
# Get recordings to check for expiration
recordings: Recordings = (
@@ -203,13 +409,18 @@ class RecordingCleanup(threading.Thread):
events: Event = (
Event.select()
.where(
Event.camera == camera, Event.end_time < expire_date, Event.has_clip
Event.camera == camera,
# need to ensure segments for all events starting
# before the expire date are included
Event.start_time < expire_date,
Event.has_clip,
)
.order_by(Event.start_time)
.objects()
)
# loop over recordings and see if they overlap with any non-expired events
# TODO: expire segments based on segment stats according to config
event_start = 0
deleted_recordings = set()
for recording in recordings.objects().iterator():
@@ -224,9 +435,9 @@ class RecordingCleanup(threading.Thread):
keep = False
break
# if the event ends after the recording starts, keep it
# if the event is in progress or ends after the recording starts, keep it
# and stop looking at events
if event.end_time >= recording.start_time:
if event.end_time is None or event.end_time >= recording.start_time:
keep = True
break
@@ -237,13 +448,30 @@ class RecordingCleanup(threading.Thread):
if event.end_time < recording.start_time:
event_start = idx
# Delete recordings outside of the retention window
if not keep:
# Delete recordings outside of the retention window or based on the retention mode
if (
not keep
or (
config.record.events.retain.mode == RetainModeEnum.motion
and recording.motion == 0
)
or (
config.record.events.retain.mode
== RetainModeEnum.active_objects
and recording.objects == 0
)
):
Path(recording.path).unlink(missing_ok=True)
deleted_recordings.add(recording.id)
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
Recordings.delete().where(Recordings.id << deleted_recordings).execute()
# delete up to 100,000 at a time
max_deletes = 100000
deleted_recordings_list = list(deleted_recordings)
for i in range(0, len(deleted_recordings_list), max_deletes):
Recordings.delete().where(
Recordings.id << deleted_recordings_list[i : i + max_deletes]
).execute()
logger.debug(f"End camera: {camera}.")
@@ -255,29 +483,32 @@ class RecordingCleanup(threading.Thread):
default_expire = (
datetime.datetime.now().timestamp()
- SECONDS_IN_DAY * self.config.record.retain_days
- SECONDS_IN_DAY * self.config.record.retain.days
)
delete_before = {}
for name, camera in self.config.cameras.items():
delete_before[name] = (
datetime.datetime.now().timestamp()
- SECONDS_IN_DAY * camera.record.retain_days
- SECONDS_IN_DAY * camera.record.retain.days
)
# find all the recordings older than the oldest recording in the db
try:
oldest_recording = (
Recordings.select().order_by(Recordings.start_time.desc()).get()
)
oldest_recording = Recordings.select().order_by(Recordings.start_time).get()
oldest_timestamp = oldest_recording.start_time
p = Path(oldest_recording.path)
oldest_timestamp = p.stat().st_mtime - 1
except DoesNotExist:
oldest_timestamp = datetime.datetime.now().timestamp()
except FileNotFoundError:
logger.warning(f"Unable to find file from recordings database: {p}")
Recordings.delete().where(Recordings.id == oldest_recording.id).execute()
return
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
process = sp.run(
["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"],
["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
capture_output=True,
text=True,
)
@@ -285,21 +516,57 @@ class RecordingCleanup(threading.Thread):
for f in files_to_check:
p = Path(f)
if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire):
p.unlink(missing_ok=True)
try:
if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire):
p.unlink(missing_ok=True)
except FileNotFoundError:
logger.warning(f"Attempted to expire missing file: {f}")
logger.debug("End expire files (legacy).")
def sync_recordings(self):
logger.debug("Start sync recordings.")
# get all recordings in the db
recordings: Recordings = Recordings.select()
# get all recordings files on disk
process = sp.run(
["find", RECORD_DIR, "-type", "f"],
capture_output=True,
text=True,
)
files_on_disk = process.stdout.splitlines()
recordings_to_delete = []
for recording in recordings.objects().iterator():
if not recording.path in files_on_disk:
recordings_to_delete.append(recording.id)
logger.debug(
f"Deleting {len(recordings_to_delete)} recordings with missing files"
)
# delete up to 100,000 at a time
max_deletes = 100000
for i in range(0, len(recordings_to_delete), max_deletes):
Recordings.delete().where(
Recordings.id << recordings_to_delete[i : i + max_deletes]
).execute()
logger.debug("End sync recordings.")
def run(self):
# Expire recordings every minute, clean directories every hour.
for counter in itertools.cycle(range(60)):
# on startup sync recordings with disk (disabled due to too much CPU usage)
# self.sync_recordings()
# Expire tmp clips every minute, recordings and clean directories every hour.
for counter in itertools.cycle(range(self.config.record.expire_interval)):
if self.stop_event.wait(60):
logger.info(f"Exiting recording cleanup...")
break
self.expire_recordings()
self.clean_tmp_clips()
if counter == 0:
self.expire_recordings()
self.expire_files()
remove_empty_directories(RECORD_DIR)

View File

@@ -4,24 +4,50 @@ import threading
import time
import psutil
import shutil
import os
import requests
from typing import Optional, Any
from paho.mqtt.client import Client
from multiprocessing.synchronize import Event
from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
from frigate.types import StatsTrackingTypes, CameraMetricsTypes
from frigate.version import VERSION
from frigate.edgetpu import EdgeTPUProcess
logger = logging.getLogger(__name__)
def stats_init(camera_metrics, detectors):
stats_tracking = {
def get_latest_version() -> str:
try:
request = requests.get(
"https://api.github.com/repos/blakeblackshear/frigate/releases/latest"
)
except:
return "unknown"
response = request.json()
if request.ok and response and "tag_name" in response:
return str(response.get("tag_name").replace("v", ""))
else:
return "unknown"
def stats_init(
camera_metrics: dict[str, CameraMetricsTypes], detectors: dict[str, EdgeTPUProcess]
) -> StatsTrackingTypes:
stats_tracking: StatsTrackingTypes = {
"camera_metrics": camera_metrics,
"detectors": detectors,
"started": int(time.time()),
"latest_frigate_version": get_latest_version(),
}
return stats_tracking
def get_fs_type(path):
def get_fs_type(path: str) -> str:
bestMatch = ""
fsType = ""
for part in psutil.disk_partitions(all=True):
@@ -31,36 +57,67 @@ def get_fs_type(path):
return fsType
def stats_snapshot(stats_tracking):
def read_temperature(path: str) -> Optional[float]:
if os.path.isfile(path):
with open(path) as f:
line = f.readline().strip()
return int(line) / 1000
return None
def get_temperatures() -> dict[str, float]:
temps = {}
# Get temperatures for all attached Corals
base = "/sys/class/apex/"
if os.path.isdir(base):
for apex in os.listdir(base):
temp = read_temperature(os.path.join(base, apex, "temp"))
if temp is not None:
temps[apex] = temp
return temps
def stats_snapshot(stats_tracking: StatsTrackingTypes) -> dict[str, Any]:
camera_metrics = stats_tracking["camera_metrics"]
stats = {}
stats: dict[str, Any] = {}
total_detection_fps = 0
for name, camera_stats in camera_metrics.items():
total_detection_fps += camera_stats["detection_fps"].value
pid = camera_stats["process"].pid if camera_stats["process"] else None
cpid = (
camera_stats["capture_process"].pid
if camera_stats["capture_process"]
else None
)
stats[name] = {
"camera_fps": round(camera_stats["camera_fps"].value, 2),
"process_fps": round(camera_stats["process_fps"].value, 2),
"skipped_fps": round(camera_stats["skipped_fps"].value, 2),
"detection_fps": round(camera_stats["detection_fps"].value, 2),
"pid": camera_stats["process"].pid,
"capture_pid": camera_stats["capture_process"].pid,
"pid": pid,
"capture_pid": cpid,
}
stats["detectors"] = {}
for name, detector in stats_tracking["detectors"].items():
pid = detector.detect_process.pid if detector.detect_process else None
stats["detectors"][name] = {
"inference_speed": round(detector.avg_inference_speed.value * 1000, 2),
"detection_start": detector.detection_start.value,
"pid": detector.detect_process.pid,
"pid": pid,
}
stats["detection_fps"] = round(total_detection_fps, 2)
stats["service"] = {
"uptime": (int(time.time()) - stats_tracking["started"]),
"version": VERSION,
"latest_version": stats_tracking["latest_frigate_version"],
"storage": {},
"temperatures": get_temperatures(),
}
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:
@@ -79,10 +136,10 @@ class StatsEmitter(threading.Thread):
def __init__(
self,
config: FrigateConfig,
stats_tracking,
mqtt_client,
topic_prefix,
stop_event,
stats_tracking: StatsTrackingTypes,
mqtt_client: Client,
topic_prefix: str,
stop_event: Event,
):
threading.Thread.__init__(self)
self.name = "frigate_stats_emitter"
@@ -92,7 +149,7 @@ class StatsEmitter(threading.Thread):
self.topic_prefix = topic_prefix
self.stop_event = stop_event
def run(self):
def run(self) -> None:
time.sleep(10)
while not self.stop_event.wait(self.config.mqtt.stats_interval):
stats = stats_snapshot(self.stats_tracking)

4
frigate/test/const.py Normal file
View File

@@ -0,0 +1,4 @@
"""Consts for testing."""
TEST_DB = "test.db"
TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"]

View File

@@ -2,6 +2,7 @@ import unittest
import numpy as np
from pydantic import ValidationError
from frigate.config import (
BirdseyeModeEnum,
FrigateConfig,
DetectorTypeEnum,
)
@@ -80,6 +81,86 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config
assert "dog" in runtime_config.cameras["back"].objects.track
def test_override_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
"birdseye": {"enabled": False, "mode": "motion"},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert not runtime_config.cameras["back"].birdseye.enabled
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
def test_override_birdseye_non_inheritable(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous", "height": 1920},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].birdseye.enabled
def test_inherit_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].birdseye.enabled
assert (
runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
)
def test_override_tracked_objects(self):
config = {
"mqtt": {"host": "mqtt"},
@@ -572,7 +653,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].motion.frame_height >= 120
assert runtime_config.cameras["back"].motion.frame_height == 50
def test_motion_contour_area_dynamic(self):
@@ -601,7 +682,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert round(runtime_config.cameras["back"].motion.contour_area) == 99
assert round(runtime_config.cameras["back"].motion.contour_area) == 30
def test_merge_labelmap(self):
@@ -702,7 +783,11 @@ class TestConfig(unittest.TestCase):
"inputs": [
{
"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 +802,87 @@ class TestConfig(unittest.TestCase):
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_works_on_missing_role_multiple_cams(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"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
},
"cam2": {
"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,
},
},
},
}
frigate_config = FrigateConfig(**config)
runtime_config = frigate_config.runtime_config
def test_global_detect(self):
config = {
@@ -958,6 +1124,34 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config
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):
config = {
@@ -1131,6 +1325,60 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].snapshots.retain.default == 1.5
def test_fails_on_bad_camera_name(self):
config = {
"mqtt": {"host": "mqtt"},
"snapshots": {"retain": {"default": 1.5}},
"cameras": {
"back camer#": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
self.assertRaises(
ValidationError, lambda: frigate_config.runtime_config.cameras
)
def test_object_filter_ratios_work(self):
config = {
"mqtt": {"host": "mqtt"},
"objects": {
"track": ["person", "dog"],
"filters": {"dog": {"min_ratio": 0.2, "max_ratio": 10.1}},
},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert "dog" in runtime_config.cameras["back"].objects.filters
assert runtime_config.cameras["back"].objects.filters["dog"].min_ratio == 0.2
assert runtime_config.cameras["back"].objects.filters["dog"].max_ratio == 10.1
if __name__ == "__main__":
unittest.main(verbosity=2)

328
frigate/test/test_http.py Normal file
View File

@@ -0,0 +1,328 @@
import datetime
import json
import logging
import os
import unittest
from unittest.mock import patch
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from playhouse.shortcuts import model_to_dict
from frigate.config import FrigateConfig
from frigate.http import create_app
from frigate.models import Event, Recordings
from frigate.plus import PlusApi
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
class TestHttp(unittest.TestCase):
def setUp(self):
# setup clean database for each test run
migrate_db = SqliteExtDatabase("test.db")
del logging.getLogger("peewee_migrate").handlers[:]
router = Router(migrate_db)
router.run()
migrate_db.close()
self.db = SqliteQueueDatabase(TEST_DB)
models = [Event, Recordings]
self.db.bind(models)
self.minimal_config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
self.test_stats = {
"detection_fps": 13.7,
"detectors": {
"cpu1": {
"detection_start": 0.0,
"inference_speed": 91.43,
"pid": 42,
},
"cpu2": {
"detection_start": 0.0,
"inference_speed": 84.99,
"pid": 44,
},
},
"front_door": {
"camera_fps": 0.0,
"capture_pid": 53,
"detection_fps": 0.0,
"pid": 52,
"process_fps": 0.0,
"skipped_fps": 0.0,
},
"service": {
"storage": {
"/dev/shm": {
"free": 50.5,
"mount_type": "tmpfs",
"total": 67.1,
"used": 16.6,
},
"/media/frigate/clips": {
"free": 42429.9,
"mount_type": "ext4",
"total": 244529.7,
"used": 189607.0,
},
"/media/frigate/recordings": {
"free": 0.2,
"mount_type": "ext4",
"total": 8.0,
"used": 7.8,
},
"/tmp/cache": {
"free": 976.8,
"mount_type": "tmpfs",
"total": 1000.0,
"used": 23.2,
},
},
"uptime": 101113,
"version": "0.10.1",
"latest_version": "0.11",
},
}
def tearDown(self):
if not self.db.is_closed():
self.db.close()
try:
for file in TEST_DB_CLEANUPS:
os.remove(file)
except OSError:
pass
def test_get_event_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
id2 = "7890.random"
with app.test_client() as client:
_insert_mock_event(id)
events = client.get(f"/events").json
assert events
assert len(events) == 1
assert events[0]["id"] == id
_insert_mock_event(id2)
events = client.get(f"/events").json
assert events
assert len(events) == 2
events = client.get(
f"/events",
query_string={"limit": 1},
).json
assert events
assert len(events) == 1
events = client.get(
f"/events",
query_string={"has_clip": 0},
).json
assert not events
def test_get_good_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event == model_to_dict(Event.get(Event.id == id))
def test_get_bad_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
bad_id = "654321.other"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{bad_id}").json
assert not event
def test_delete_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
client.delete(f"/events/{id}")
event = client.get(f"/events/{id}").json
assert not event
def test_event_retention(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
client.post(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == True
client.delete(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == False
def test_set_delete_sub_label(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == sub_label
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": ""}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == ""
def test_sub_label_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
sub_labels = client.get("/sub_labels").json
assert sub_labels
assert sub_labels == [sub_label]
def test_config(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
with app.test_client() as client:
config = client.get("/config").json
assert config
assert config["cameras"]["front_door"]
def test_recordings(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_recording(id)
recording = client.get("/front_door/recordings").json
assert recording
assert recording[0]["id"] == id
@patch("frigate.http.stats_snapshot")
def test_stats(self, mock_stats):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
mock_stats.return_value = self.test_stats
with app.test_client() as client:
stats = client.get("/stats").json
assert stats == self.test_stats
def _insert_mock_event(id: str) -> Event:
"""Inserts a basic event model with a given id."""
return Event.insert(
id=id,
label="Mock",
camera="front_door",
start_time=datetime.datetime.now().timestamp(),
end_time=datetime.datetime.now().timestamp() + 20,
top_score=100,
false_positive=False,
zones=list(),
thumbnail="",
region=[],
box=[],
area=0,
has_clip=True,
has_snapshot=True,
).execute()
def _insert_mock_recording(id: str) -> Event:
"""Inserts a basic recording model with a given id."""
return Recordings.insert(
id=id,
camera="front_door",
path=f"/recordings/{id}",
start_time=datetime.datetime.now().timestamp() - 50,
end_time=datetime.datetime.now().timestamp() - 60,
duration=10,
motion=True,
objects=True,
).execute()

View File

@@ -0,0 +1,26 @@
import numpy as np
from unittest import TestCase, main
from frigate.video import box_overlaps, reduce_boxes
class TestBoxOverlaps(TestCase):
def test_overlap(self):
assert box_overlaps((100, 100, 200, 200), (50, 50, 150, 150))
def test_overlap_2(self):
assert box_overlaps((50, 50, 150, 150), (100, 100, 200, 200))
def test_no_overlap(self):
assert not box_overlaps((100, 100, 200, 200), (250, 250, 350, 350))
class TestReduceBoxes(TestCase):
def test_cluster(self):
clusters = reduce_boxes(
[(144, 290, 221, 459), (225, 178, 426, 341), (343, 105, 584, 250)]
)
assert len(clusters) == 2
if __name__ == "__main__":
main(verbosity=2)

View File

@@ -38,4 +38,4 @@ class TestYuvRegion2RGB(TestCase):
if __name__ == "__main__":
main(verbosity=2)
main(verbosity=2)

31
frigate/types.py Normal file
View File

@@ -0,0 +1,31 @@
from typing import Optional, TypedDict
from multiprocessing.queues import Queue
from multiprocessing.sharedctypes import Synchronized
from multiprocessing.context import Process
from frigate.edgetpu import EdgeTPUProcess
class CameraMetricsTypes(TypedDict):
camera_fps: Synchronized
capture_process: Optional[Process]
detection_enabled: Synchronized
detection_fps: Synchronized
detection_frame: Synchronized
ffmpeg_pid: Synchronized
frame_queue: Queue
motion_enabled: Synchronized
improve_contrast_enabled: Synchronized
motion_threshold: Synchronized
motion_contour_area: Synchronized
process: Optional[Process]
process_fps: Synchronized
read_start: Synchronized
skipped_fps: Synchronized
class StatsTrackingTypes(TypedDict):
camera_metrics: dict[str, CameraMetricsTypes]
detectors: dict[str, EdgeTPUProcess]
started: int
latest_frigate_version: str

View File

@@ -1,4 +1,3 @@
import collections
import copy
import datetime
import hashlib
@@ -11,6 +10,7 @@ import threading
import time
import traceback
from abc import ABC, abstractmethod
from collections.abc import Mapping
from multiprocessing import shared_memory
from typing import AnyStr
@@ -34,7 +34,7 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic
for k, v2 in dct2.items():
if k in merged:
v1 = merged[k]
if isinstance(v1, dict) and isinstance(v2, collections.Mapping):
if isinstance(v1, dict) and isinstance(v2, Mapping):
merged[k] = deep_merge(v1, v2, override)
elif isinstance(v1, list) and isinstance(v2, list):
if merge_lists:
@@ -189,12 +189,12 @@ def draw_box_with_label(
)
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2):
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, model_size, multiplier=2):
# size is the longest edge and divisible by 4
size = int(max(xmax - xmin, ymax - ymin) // 4 * 4 * multiplier)
# dont go any smaller than 300
if size < 300:
size = 300
size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4)
# dont go any smaller than the model_size
if size < model_size:
size = model_size
# x_offset is midpoint of bounding box minus half the size
x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0)
@@ -522,7 +522,7 @@ def clipped(obj, frame_shape):
# if the object is within 5 pixels of the region border, and the region is not on the edge
# consider the object to be clipped
box = obj[2]
region = obj[4]
region = obj[5]
if (
(region[0] > 5 and box[0] - region[0] <= 5)
or (region[1] > 5 and box[1] - region[1] <= 5)
@@ -567,6 +567,9 @@ class EventsPerSecond:
# compute the (approximate) events in the last n seconds
now = datetime.datetime.now().timestamp()
seconds = min(now - self._start, last_n_seconds)
# avoid divide by zero
if seconds == 0:
seconds = 1
return (
len([t for t in self._timestamps if t > (now - last_n_seconds)]) / seconds
)
@@ -602,6 +605,26 @@ def add_mask(mask, mask_img):
cv2.fillPoly(mask_img, pts=[contour], color=(0))
def load_labels(path, encoding="utf-8"):
"""Loads labels from file (with or without index numbers).
Args:
path: path to label file.
encoding: label file encoding.
Returns:
Dictionary mapping indices to labels.
"""
with open(path, "r", encoding=encoding) as f:
lines = f.readlines()
if not lines:
return {}
if lines[0].split(" ", maxsplit=1)[0].isdigit():
pairs = [line.split(" ", maxsplit=1) for line in lines]
return {int(index): label.strip() for index, label in pairs}
else:
return {index: line.strip() for index, line in enumerate(lines)}
class FrameManager(ABC):
@abstractmethod
def create(self, name, size) -> AnyStr:

View File

@@ -3,18 +3,18 @@ import itertools
import logging
import multiprocessing as mp
import queue
import subprocess as sp
import random
import signal
import subprocess as sp
import threading
import time
from collections import defaultdict
from setproctitle import setproctitle
from typing import Dict, List
from cv2 import cv2
import numpy as np
from cv2 import cv2, reduce
from setproctitle import setproctitle
from frigate.config import CameraConfig
from frigate.config import CameraConfig, DetectConfig
from frigate.edgetpu import RemoteObjectDetector
from frigate.log import LogPipe
from frigate.motion import MotionDetector
@@ -23,8 +23,11 @@ from frigate.util import (
EventsPerSecond,
FrameManager,
SharedMemoryFrameManager,
area,
calculate_region,
clipped,
intersection,
intersection_over_union,
listen,
yuv_region_2_rgb,
)
@@ -34,6 +37,10 @@ logger = logging.getLogger(__name__)
def filtered(obj, objects_to_track, object_filters):
object_name = obj[0]
object_score = obj[1]
object_box = obj[2]
object_area = obj[3]
object_ratio = obj[4]
if not object_name in objects_to_track:
return True
@@ -43,24 +50,35 @@ def filtered(obj, objects_to_track, object_filters):
# if the min area is larger than the
# detected object, don't add it to detected objects
if obj_settings.min_area > obj[3]:
if obj_settings.min_area > object_area:
return True
# if the detected object is larger than the
# max area, don't add it to detected objects
if obj_settings.max_area < obj[3]:
if obj_settings.max_area < object_area:
return True
# if the score is lower than the min_score, skip
if obj_settings.min_score > obj[1]:
if obj_settings.min_score > object_score:
return True
# if the object is not proportionally wide enough
if obj_settings.min_ratio > object_ratio:
return True
# if the object is proportionally too wide
if obj_settings.max_ratio < object_ratio:
return True
if not obj_settings.mask is None:
# compute the coordinates of the object and make sure
# the location isnt outside the bounds of the image (can happen from rounding)
y_location = min(int(obj[2][3]), len(obj_settings.mask) - 1)
# the location isn't outside the bounds of the image (can happen from rounding)
object_xmin = object_box[0]
object_xmax = object_box[2]
object_ymax = object_box[3]
y_location = min(int(object_ymax), len(obj_settings.mask) - 1)
x_location = min(
int((obj[2][2] - obj[2][0]) / 2.0) + obj[2][0],
int((object_xmax + object_xmin) / 2.0),
len(obj_settings.mask[0]) - 1,
)
@@ -149,10 +167,10 @@ def capture_frames(
try:
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
except Exception as e:
logger.info(f"{camera_name}: ffmpeg sent a broken frame. {e}")
logger.error(f"{camera_name}: Unable to read frames from ffmpeg process.")
if ffmpeg_process.poll() != None:
logger.info(
logger.error(
f"{camera_name}: ffmpeg process is not running. exiting capture thread..."
)
frame_manager.delete(frame_name)
@@ -184,7 +202,7 @@ class CameraWatchdog(threading.Thread):
self.config = config
self.capture_thread = None
self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR)
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect")
self.ffmpeg_other_processes = []
self.camera_fps = camera_fps
self.ffmpeg_pid = ffmpeg_pid
@@ -200,8 +218,7 @@ class CameraWatchdog(threading.Thread):
if "detect" in c["roles"]:
continue
logpipe = LogPipe(
f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}",
logging.ERROR,
f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}"
)
self.ffmpeg_other_processes.append(
{
@@ -217,12 +234,11 @@ class CameraWatchdog(threading.Thread):
if not self.capture_thread.is_alive():
self.logger.error(
f"FFMPEG process crashed unexpectedly for {self.camera_name}."
f"Ffmpeg process crashed unexpectedly for {self.camera_name}."
)
self.logger.error(
"The following ffmpeg logs include the last 100 lines prior to exit."
)
self.logger.error("You may have invalid args defined for this camera.")
self.logpipe.dump()
self.start_ffmpeg_detect()
elif now - self.capture_thread.current_frame.value > 20:
@@ -345,12 +361,22 @@ def track_camera(
frame_queue = process_info["frame_queue"]
detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"]
motion_threshold = process_info["motion_threshold"]
motion_contour_area = process_info["motion_contour_area"]
frame_shape = config.frame_shape
objects_to_track = config.objects.track
object_filters = config.objects.filters
motion_detector = MotionDetector(frame_shape, config.motion)
motion_detector = MotionDetector(
frame_shape,
config.motion,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
)
object_detector = RemoteObjectDetector(
name, labelmap, detection_queue, result_connection, model_shape
)
@@ -364,6 +390,7 @@ def track_camera(
frame_queue,
frame_shape,
model_shape,
config.detect,
frame_manager,
motion_detector,
object_detector,
@@ -373,32 +400,43 @@ def track_camera(
objects_to_track,
object_filters,
detection_enabled,
motion_enabled,
stop_event,
)
logger.info(f"{name}: exiting subprocess")
def reduce_boxes(boxes):
if len(boxes) == 0:
return []
reduced_boxes = cv2.groupRectangles(
[list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2
)[0]
return [tuple(b) for b in reduced_boxes]
def box_overlaps(b1, b2):
if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
return False
return True
def reduce_boxes(boxes, iou_threshold=0.0):
clusters = []
for box in boxes:
matched = 0
for cluster in clusters:
if intersection_over_union(box, cluster) > iou_threshold:
matched = 1
cluster[0] = min(cluster[0], box[0])
cluster[1] = min(cluster[1], box[1])
cluster[2] = max(cluster[2], box[2])
cluster[3] = max(cluster[3], box[3])
if not matched:
clusters.append(list(box))
return [tuple(c) for c in clusters]
# modified from https://stackoverflow.com/a/40795835
def intersects_any(box_a, boxes):
for box in boxes:
if (
box_a[2] < box[0]
or box_a[0] > box[2]
or box_a[1] > box[3]
or box_a[3] < box[1]
):
continue
return True
if box_overlaps(box_a, box):
return True
return False
def detect(
@@ -415,11 +453,16 @@ def detect(
y_min = int((box[0] * size) + region[1])
x_max = int((box[3] * size) + region[0])
y_max = int((box[2] * size) + region[1])
width = x_max - x_min
height = y_max - y_min
area = width * height
ratio = width / height
det = (
d[0],
d[1],
(x_min, y_min, x_max, y_max),
(x_max - x_min) * (y_max - y_min),
area,
ratio,
region,
)
# apply object filters
@@ -434,15 +477,17 @@ def process_frames(
frame_queue: mp.Queue,
frame_shape,
model_shape,
detect_config: DetectConfig,
frame_manager: FrameManager,
motion_detector: MotionDetector,
object_detector: RemoteObjectDetector,
object_tracker: ObjectTracker,
detected_objects_queue: mp.Queue,
process_info: Dict,
objects_to_track: List[str],
process_info: dict,
objects_to_track: list[str],
object_filters,
detection_enabled: mp.Value,
motion_enabled: mp.Value,
stop_event,
exit_on_empty: bool = False,
):
@@ -454,6 +499,8 @@ def process_frames(
fps_tracker = EventsPerSecond()
fps_tracker.start()
startup_scan_counter = 0
while not stop_event.is_set():
if exit_on_empty and frame_queue.empty():
logger.info(f"Exiting track_objects...")
@@ -474,122 +521,227 @@ def process_frames(
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
continue
# look for motion if enabled
motion_boxes = motion_detector.detect(frame) if motion_enabled.value else []
regions = []
# if detection is disabled
if not detection_enabled.value:
fps.value = fps_tracker.eps()
object_tracker.match_and_update(frame_time, [])
detected_objects_queue.put(
(camera_name, frame_time, object_tracker.tracked_objects, [], [])
)
detection_fps.value = object_detector.fps.eps()
frame_manager.close(f"{camera_name}{frame_time}")
continue
# look for motion
motion_boxes = motion_detector.detect(frame)
# only get the tracked object boxes that intersect with motion
tracked_object_boxes = [
obj["box"]
for obj in object_tracker.tracked_objects.values()
if intersects_any(obj["box"], motion_boxes)
]
# combine motion boxes with known locations of existing objects
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
# compute regions
regions = [
calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2)
for a in combined_boxes
]
# combine overlapping regions
combined_regions = reduce_boxes(regions)
# re-compute regions
regions = [
calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0)
for a in combined_regions
]
# resize regions and detect
detections = []
for region in regions:
detections.extend(
detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
else:
# get stationary object ids
# check every Nth frame for stationary objects
# disappeared objects are not stationary
# also check for overlapping motion boxes
stationary_object_ids = [
obj["id"]
for obj in object_tracker.tracked_objects.values()
# if there hasn't been motion for 10 frames
if obj["motionless_count"] >= 10
# and it isn't due for a periodic check
and (
detect_config.stationary.interval == 0
or obj["motionless_count"] % detect_config.stationary.interval != 0
)
)
# and it hasn't disappeared
and object_tracker.disappeared[obj["id"]] == 0
# and it doesn't overlap with any current motion boxes
and not intersects_any(obj["box"], motion_boxes)
]
#########
# merge objects, check for clipped objects and look again up to 4 times
#########
refining = True
refine_count = 0
while refining and refine_count < 4:
refining = False
# get tracked object boxes that aren't stationary
tracked_object_boxes = [
obj["box"]
for obj in object_tracker.tracked_objects.values()
if not obj["id"] in stationary_object_ids
]
# group by name
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
detected_object_groups[detection[0]].append(detection)
# combine motion boxes with known locations of existing objects
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
selected_objects = []
for group in detected_object_groups.values():
region_min_size = max(model_shape[0], model_shape[1])
# compute regions
regions = [
calculate_region(
frame_shape,
a[0],
a[1],
a[2],
a[3],
region_min_size,
multiplier=random.uniform(1.2, 1.5),
)
for a in combined_boxes
]
# apply non-maxima suppression to suppress weak, overlapping bounding boxes
boxes = [
(o[2][0], o[2][1], o[2][2] - o[2][0], o[2][3] - o[2][1])
for o in group
]
confidences = [o[1] for o in group]
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
# consolidate regions with heavy overlap
regions = [
calculate_region(
frame_shape, a[0], a[1], a[2], a[3], region_min_size, multiplier=1.0
)
for a in reduce_boxes(regions, 0.4)
]
for index in idxs:
obj = group[index[0]]
if clipped(obj, frame_shape):
box = obj[2]
# calculate a new region that will hopefully get the entire object
region = calculate_region(
frame_shape, box[0], box[1], box[2], box[3]
# if starting up, get the next startup scan region
if startup_scan_counter < 9:
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)
ymax = int(frame_shape[0] / 3 + ymin)
xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3)
xmax = int(frame_shape[1] / 3 + xmin)
regions.append(
calculate_region(
frame_shape,
xmin,
ymin,
xmax,
ymax,
region_min_size,
multiplier=1.2,
)
)
startup_scan_counter += 1
# resize regions and detect
# seed with stationary objects
detections = [
(
obj["label"],
obj["score"],
obj["box"],
obj["area"],
obj["ratio"],
obj["region"],
)
for obj in object_tracker.tracked_objects.values()
if obj["id"] in stationary_object_ids
]
for region in regions:
detections.extend(
detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
)
)
#########
# merge objects, check for clipped objects and look again up to 4 times
#########
refining = len(regions) > 0
refine_count = 0
while refining and refine_count < 4:
refining = False
# group by name
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
detected_object_groups[detection[0]].append(detection)
selected_objects = []
for group in detected_object_groups.values():
# apply non-maxima suppression to suppress weak, overlapping bounding boxes
# o[2] is the box of the object: xmin, ymin, xmax, ymax
boxes = [
(
o[2][0],
o[2][1],
o[2][2] - o[2][0],
o[2][3] - o[2][1],
)
for o in group
]
confidences = [o[1] for o in group]
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
regions.append(region)
selected_objects.extend(
detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
for index in idxs:
index = index if isinstance(index, np.int32) else index[0]
obj = group[index]
if clipped(obj, frame_shape):
box = obj[2]
# calculate a new region that will hopefully get the entire object
region = calculate_region(
frame_shape,
box[0],
box[1],
box[2],
box[3],
region_min_size,
)
)
refining = True
else:
selected_objects.append(obj)
# set the detections list to only include top, complete objects
# and new detections
detections = selected_objects
regions.append(region)
if refining:
refine_count += 1
selected_objects.extend(
detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
)
)
# Limit to the detections overlapping with motion areas
# to avoid picking up stationary background objects
detections_with_motion = [
d for d in detections if intersects_any(d[2], motion_boxes)
]
refining = True
else:
selected_objects.append(obj)
# set the detections list to only include top, complete objects
# and new detections
detections = selected_objects
# now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, detections_with_motion)
if refining:
refine_count += 1
## drop detections that overlap too much
consolidated_detections = []
# if detection was run on this frame, consolidate
if len(regions) > 0:
# group by name
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
detected_object_groups[detection[0]].append(detection)
# loop over detections grouped by label
for group in detected_object_groups.values():
# if the group only has 1 item, skip
if len(group) == 1:
consolidated_detections.append(group[0])
continue
# sort smallest to largest by area
sorted_by_area = sorted(group, key=lambda g: g[3])
for current_detection_idx in range(0, len(sorted_by_area)):
current_detection = sorted_by_area[current_detection_idx][2]
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
len(sorted_by_area),
):
to_check = sorted_by_area[to_check_idx][2]
# if 90% of smaller detection is inside of another detection, consolidate
if (
area(intersection(current_detection, to_check))
/ area(current_detection)
> 0.9
):
overlap = 1
break
if overlap == 0:
consolidated_detections.append(
sorted_by_area[current_detection_idx]
)
# now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, consolidated_detections)
# else, just update the frame times for the stationary objects
else:
object_tracker.update_frame_times(frame_time)
# add to the queue if not full
if detected_objects_queue.full():

View File

@@ -5,21 +5,21 @@ import time
import os
import signal
from frigate.util import (
restart_frigate,
)
from frigate.edgetpu import EdgeTPUProcess
from frigate.util import restart_frigate
from multiprocessing.synchronize import Event
logger = logging.getLogger(__name__)
class FrigateWatchdog(threading.Thread):
def __init__(self, detectors, stop_event):
def __init__(self, detectors: dict[str, EdgeTPUProcess], stop_event: Event):
threading.Thread.__init__(self)
self.name = "frigate_watchdog"
self.detectors = detectors
self.stop_event = stop_event
def run(self):
def run(self) -> None:
time.sleep(10)
while not self.stop_event.wait(10):
now = datetime.datetime.now().timestamp()
@@ -32,7 +32,10 @@ class FrigateWatchdog(threading.Thread):
"Detection appears to be stuck. Restarting detection process..."
)
detector.start_or_restart()
elif not detector.detect_process.is_alive():
elif (
detector.detect_process is not None
and not detector.detect_process.is_alive()
):
logger.info("Detection appears to have stopped. Exiting frigate...")
restart_frigate()

View File

@@ -14,38 +14,41 @@ logger = logging.getLogger(__name__)
ZEROCONF_TYPE = "_frigate._tcp.local."
# Taken from: http://stackoverflow.com/a/11735897
def get_local_ip() -> str:
def get_local_ip() -> bytes:
"""Try to determine the local IP address of the machine."""
host_ip_str = ""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(("8.8.8.8", 80))
return sock.getsockname()[0] # type: ignore
host_ip_str = sock.getsockname()[0]
except OSError:
try:
return socket.gethostbyname(socket.gethostname())
host_ip_str = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
return "127.0.0.1"
host_ip_str = "127.0.0.1"
finally:
sock.close()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip_str)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip_str)
def broadcast_zeroconf(frigate_id):
return host_ip_pton
def broadcast_zeroconf(frigate_id: str) -> Zeroconf:
zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only)
host_ip = get_local_ip()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
info = ServiceInfo(
ZEROCONF_TYPE,
name=f"{frigate_id}.{ZEROCONF_TYPE}",
addresses=[host_ip_pton],
addresses=[host_ip],
port=5000,
)
@@ -56,4 +59,4 @@ def broadcast_zeroconf(frigate_id):
logger.error(
"Frigate instance with identical name present in the local network"
)
return zeroconf
return zeroconf

View File

@@ -28,17 +28,19 @@ SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.create_model(Recordings)
def add_index():
# First add the index here, because there is a bug in peewee_migrate
# when trying to create an multi-column index in the same migration
# as the table: https://github.com/klen/peewee_migrate/issues/19
Recordings.add_index("start_time", "end_time")
Recordings.create_table()
migrator.python(add_index)
migrator.sql(
'CREATE TABLE IF NOT EXISTS "recordings" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)'
)
migrator.sql(
'CREATE INDEX IF NOT EXISTS "recordings_camera" ON "recordings" ("camera")'
)
migrator.sql(
'CREATE UNIQUE INDEX IF NOT EXISTS "recordings_path" ON "recordings" ("path")'
)
migrator.sql(
'CREATE INDEX IF NOT EXISTS "recordings_start_time_end_time" ON "recordings" (start_time, end_time)'
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_model(Recordings)
pass

View File

@@ -0,0 +1,43 @@
"""Peewee migrations -- 004_add_bbox_region_area.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.drop_not_null(Event, "end_time")
def rollback(migrator, database, fake=False, **kwargs):
pass

View File

@@ -0,0 +1,47 @@
"""Peewee migrations -- 004_add_bbox_region_area.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Recordings
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Recordings,
objects=pw.IntegerField(null=True),
motion=pw.IntegerField(null=True),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Recordings, ["objects", "motion"])

View File

@@ -0,0 +1,46 @@
"""Peewee migrations -- 007_add_retain_indefinitely.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
retain_indefinitely=pw.BooleanField(default=False),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["retain_indefinitely"])

View File

@@ -0,0 +1,46 @@
"""Peewee migrations -- 008_add_sub_label.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
sub_label=pw.CharField(max_length=20, null=True),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["sub_label"])

Some files were not shown because too many files have changed in this diff Show More