diff options
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2305.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/video/frigate.nix | 368 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/frigate.nix | 60 | ||||
-rw-r--r-- | pkgs/applications/video/frigate/default.nix | 179 | ||||
-rw-r--r-- | pkgs/applications/video/frigate/web.nix | 26 | ||||
-rw-r--r-- | pkgs/servers/http/nginx/modules.nix | 11 | ||||
-rw-r--r-- | pkgs/top-level/all-packages.nix | 2 |
9 files changed, 645 insertions, 5 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index a9d4f99b798bd..9d7387a75af6e 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -54,6 +54,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [system-repart](https://www.freedesktop.org/software/systemd/man/systemd-repart.service.html), grow and add partitions to a partition table. Available as [systemd.repart](options.html#opt-systemd.repart) and [boot.initrd.systemd.repart](options.html#opt-boot.initrd.systemd.repart) +- [frigate](https://frigate.video), an open source NVR built around real-time AI object detection. Available as [services.frigate](#opt-services.frigate.enable). + - [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion). - [readarr](https://github.com/Readarr/Readarr), Book Manager and Automation (Sonarr for Ebooks). Available as [services.readarr](options.html#opt-services.readarr.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b83e30a3b5a66..ff06a72ff9dd5 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1151,6 +1151,7 @@ ./services/ttys/kmscon.nix ./services/video/epgstation/default.nix ./services/video/go2rtc/default.nix + ./services/video/frigate.nix ./services/video/mirakurun.nix ./services/video/replay-sorcery.nix ./services/video/mediamtx.nix diff --git a/nixos/modules/services/video/frigate.nix b/nixos/modules/services/video/frigate.nix new file mode 100644 index 0000000000000..217637cbebcf9 --- /dev/null +++ b/nixos/modules/services/video/frigate.nix @@ -0,0 +1,368 @@ +{ config +, lib +, pkgs +, ... +}: + +let + inherit (lib) + literalExpression + mkDefault + mdDoc + mkEnableOption + mkIf + mkOption + types; + + cfg = config.services.frigate; + + format = pkgs.formats.yaml {}; + + filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! lib.elem v [ null ])) cfg.settings; + + cameraFormat = with types; submodule { + freeformType = format.type; + options = { + ffmpeg = { + inputs = mkOption { + description = mdDoc '' + List of inputs for this camera. + ''; + type = listOf (submodule { + freeformType = format.type; + options = { + path = mkOption { + type = str; + example = "rtsp://192.0.2.1:554/rtsp"; + description = mdDoc '' + Stream URL + ''; + }; + roles = mkOption { + type = listOf (enum [ "detect" "record" "rtmp" ]); + example = literalExpression '' + [ "detect" "rtmp" ] + ''; + description = mdDoc '' + List of roles for this stream + ''; + }; + }; + }); + }; + }; + }; + }; + +in + +{ + meta.buildDocsInSandbox = false; + + options.services.frigate = with types; { + enable = mkEnableOption (mdDoc "Frigate NVR"); + + package = mkOption { + type = package; + default = pkgs.frigate; + description = mdDoc '' + The frigate package to use. + ''; + }; + + hostname = mkOption { + type = str; + example = "frigate.exampe.com"; + description = mdDoc '' + Hostname of the nginx vhost to configure. + + Only nginx is supported by upstream for direct reverse proxying. + ''; + }; + + settings = mkOption { + type = submodule { + freeformType = format.type; + options = { + cameras = mkOption { + type = attrsOf cameraFormat; + description = mdDoc '' + Attribute set of cameras configurations. + + https://docs.frigate.video/configuration/cameras + ''; + }; + + database = { + path = mkOption { + type = path; + default = "/var/lib/frigate/frigate.db"; + description = mdDoc '' + Path to the SQLite database used + ''; + }; + }; + + mqtt = { + enabled = mkEnableOption (mdDoc "MQTT support"); + + host = mkOption { + type = nullOr str; + default = null; + example = "mqtt.example.com"; + description = mdDoc '' + MQTT server hostname + ''; + }; + }; + }; + }; + default = {}; + description = mdDoc '' + Frigate configuration as a nix attribute set. + + See the project documentation for how to configure frigate. + - [Creating a config file](https://docs.frigate.video/guides/getting_started) + - [Configuration reference](https://docs.frigate.video/configuration/index) + ''; + }; + }; + + config = mkIf cfg.enable { + services.nginx = { + enable =true; + additionalModules = with pkgs.nginxModules; [ + secure-token + rtmp + vod + ]; + recommendedProxySettings = mkDefault true; + recommendedGzipSettings = mkDefault true; + upstreams = { + frigate-api.servers = { + "127.0.0.1:5001" = {}; + }; + frigate-mqtt-ws.servers = { + "127.0.0.1:5002" = {}; + }; + frigate-jsmpeg.servers = { + "127.0.0.1:8082" = {}; + }; + frigate-go2rtc.servers = { + "127.0.0.1:1984" = {}; + }; + }; + # Based on https://github.com/blakeblackshear/frigate/blob/v0.12.0/docker/rootfs/usr/local/nginx/conf/nginx.conf + virtualHosts."${cfg.hostname}" = { + locations = { + "/api/" = { + proxyPass = "http://frigate-api/"; + }; + "~* /api/.*\.(jpg|jpeg|png)$" = { + proxyPass = "http://frigate-api"; + extraConfig = '' + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; + rewrite ^/api/(.*)$ $1 break; + ''; + }; + "/vod/" = { + extraConfig = '' + aio threads; + vod hls; + + secure_token $args; + secure_token_types application/vnd.apple.mpegurl; + + add_header Access-Control-Allow-Headers '*'; + 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 '*'; + add_header Cache-Control "no-store"; + expires off; + ''; + }; + "/stream/" = { + # TODO + }; + "/ws" = { + proxyPass = "http://frigate-mqtt-ws/"; + proxyWebsockets = true; + }; + "/live/jsmpeg" = { + proxyPass = "http://frigate-jsmpeg/"; + proxyWebsockets = true; + }; + "/live/mse/" = { + proxyPass = "http://frigate-go2rtc/"; + proxyWebsockets = true; + }; + "/live/webrtc/" = { + proxyPass = "http://frigate-go2rtc/"; + proxyWebsockets = true; + }; + "/cache/" = { + alias = "/var/cache/frigate/"; + }; + "/clips/" = { + root = "/var/lib/frigate"; + extraConfig = '' + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Expose-Headers' 'Content-Length'; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' "$http_origin"; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + types { + video/mp4 mp4; + image/jpeg jpg; + } + + autoindex on; + ''; + }; + "/recordings/" = { + root = "/var/lib/frigate"; + extraConfig = '' + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header 'Access-Control-Expose-Headers' 'Content-Length'; + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' "$http_origin"; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + types { + video/mp4 mp4; + } + + autoindex on; + autoindex_format json; + ''; + }; + "/assets/" = { + root = cfg.package.web; + extraConfig = '' + access_log off; + expires 1y; + add_header Cache-Control "public"; + ''; + }; + "/" = { + root = cfg.package.web; + tryFiles = "$uri $uri/ /index.html"; + extraConfig = '' + add_header Cache-Control "no-store"; + expires off; + + 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/monacoeditorwork/' '"$http_x_ingress_path/assets/'; + sub_filter 'return"/BASE_PATH/"' 'return 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; + ''; + }; + }; + extraConfig = '' + # vod settings + vod_base_url ""; + vod_segments_base_url ""; + vod_mode mapped; + vod_max_mapping_response_size 1m; + vod_upstream_location /api; + vod_align_segments_to_key_frames on; + vod_manifest_segment_durations_mode accurate; + vod_ignore_edit_list on; + vod_segment_duration 10000; + vod_hls_mpegts_align_frames off; + vod_hls_mpegts_interleave_frames on; + # file handle caching / aio + open_file_cache max=1000 inactive=5m; + open_file_cache_valid 2m; + open_file_cache_min_uses 1; + open_file_cache_errors on; + aio on; + # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool + vod_open_file_thread_pool default; + # vod caches + vod_metadata_cache metadata_cache 512m; + vod_mapping_cache mapping_cache 5m 10m; + # gzip manifest + gzip_types application/vnd.apple.mpegurl; + ''; + }; + appendConfig = '' + rtmp { + server { + listen 1935; + chunk_size 4096; + allow publish 127.0.0.1; + deny publish all; + allow play all; + application live { + live on; + record off; + meta copy; + } + } + } + ''; + }; + + systemd.services.frigate = { + after = [ + "go2rtc.service" + "network.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + environment = { + CONFIG_FILE = format.generate "frigate.yml" filteredConfig; + HOME = "/var/lib/frigate"; + PYTHONPATH = cfg.package.pythonPath; + }; + path = with pkgs; [ + # unfree: + # config.boot.kernelPackages.nvidiaPackages.latest.bin + ffmpeg_5-headless + libva-utils + procps + radeontop + ] ++ lib.optionals (!stdenv.isAarch64) [ + # not available on aarch64-linux + intel-gpu-tools + ]; + serviceConfig = { + ExecStart = "${cfg.package.python.interpreter} -m frigate"; + + DynamicUser = true; + User = "frigate"; + + StateDirectory = "frigate"; + UMask = "0077"; + + # Caches + PrivateTmp = true; + CacheDirectory = "frigate"; + + BindPaths = [ + "/migrations:${cfg.package}/share/frigate/migrations:ro" + ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ec012522f0a30..af541737e12f4 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -273,6 +273,7 @@ in { freeswitch = handleTest ./freeswitch.nix {}; freshrss-sqlite = handleTest ./freshrss-sqlite.nix {}; freshrss-pgsql = handleTest ./freshrss-pgsql.nix {}; + frigate = handleTest ./frigate.nix {}; frr = handleTest ./frr.nix {}; fsck = handleTest ./fsck.nix {}; fsck-systemd-stage-1 = handleTest ./fsck.nix { systemdStage1 = true; }; diff --git a/nixos/tests/frigate.nix b/nixos/tests/frigate.nix new file mode 100644 index 0000000000000..836fe0d063f87 --- /dev/null +++ b/nixos/tests/frigate.nix @@ -0,0 +1,60 @@ +import ./make-test-python.nix ({ pkgs, lib, ...} : + +{ + name = "frigate"; + meta.maintainers = with lib.maintainers; [ hexa ]; + + nodes = { + machine = { config, ... }: { + services.frigate = { + enable = true; + + hostname = "localhost"; + + settings = { + mqtt.enabled = false; + + cameras.test = { + ffmpeg = { + input_args = "-fflags nobuffer -strict experimental -fflags +genpts+discardcorrupt -r 10 -use_wallclock_as_timestamps 1"; + inputs = [ { + path = "http://127.0.0.1:8080"; + roles = [ + "record" + ]; + } ]; + }; + }; + + record.enabled = true; + }; + }; + + systemd.services.video-stream = { + description = "Start a test stream that frigate can capture"; + before = [ + "frigate.service" + ]; + wantedBy = [ + "multi-user.target" + ]; + serviceConfig = { + DynamicUser = true; + ExecStart = "${lib.getBin pkgs.ffmpeg-headless}/bin/ffmpeg -re -f lavfi -i smptebars=size=800x600:rate=10 -f mpegts -listen 1 http://0.0.0.0:8080"; + }; + }; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("frigate.service") + + machine.wait_for_open_port(5001) + + machine.succeed("curl http://localhost:5001") + + machine.wait_for_file("/var/cache/frigate/test-*.mp4") + ''; +}) diff --git a/pkgs/applications/video/frigate/default.nix b/pkgs/applications/video/frigate/default.nix new file mode 100644 index 0000000000000..05f4c16a63adc --- /dev/null +++ b/pkgs/applications/video/frigate/default.nix @@ -0,0 +1,179 @@ +{ lib +, callPackage +, python3 +, fetchFromGitHub +, fetchurl +, fetchpatch +, frigate +, opencv4 +, nixosTests +}: + +let + version = "0.12.0"; + + src = fetchFromGitHub { + #name = "frigate-${version}-source"; + owner = "blakeblackshear"; + repo = "frigate"; + rev = "refs/tags/v${version}"; + hash = "sha256-kJ0MnmWThiFbXvrN+zL5pZHq+Ig3DhCc8wPlWX2+nP8="; + }; + + frigate-web = callPackage ./web.nix { + inherit version src; + }; + + python = python3.override { + packageOverrides = self: super: { + # https://github.com/blakeblackshear/frigate/blob/v0.12.0/requirements-wheels.txt#L7 + opencv = super.toPythonModule ((opencv4.override { + enablePython = true; + pythonPackages = self; + }).overrideAttrs (oldAttrs: rec { + version = "4.5.5"; + src = fetchFromGitHub { + owner = "opencv"; + repo = "opencv"; + rev = "refs/tags/${version}"; + hash = "sha256-TJfzEAMh4JSshZ7oEZPgB59+NBACsj6Z5TCzVOBaEP4="; + }; + contribSrc = fetchFromGitHub { + owner = "opencv"; + repo = "opencv_contrib"; + rev = "refs/tags/${version}"; + hash = "sha256-skuH9GYg0mivGaJjxbggXk4x/0bbQISrAawA3ZUGfCk="; + }; + postUnpack = '' + cp --no-preserve=mode -r "${contribSrc}/modules" "$NIX_BUILD_TOP/source/opencv_contrib" + ''; + })); + }; + }; + + # Tensorflow Lite models + # https://github.com/blakeblackshear/frigate/blob/v0.12.0/Dockerfile#L88-L91 + tflite_cpu_model = fetchurl { + url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite"; + hash = "sha256-kLszpjTgQZFMwYGapd+ZgY5sOWxNLblSwP16nP/Eck8="; + }; + tflite_edgetpu_model = fetchurl { + url = "https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite"; + hash = "sha256-Siviu7YU5XbVbcuRT6UnUr8PE0EVEnENNV2X+qGzVkE="; + }; + + # OpenVino models + # https://github.com/blakeblackshear/frigate/blob/v0.12.0/Dockerfile#L92-L95 + openvino_model = fetchurl { + url = "https://github.com/openvinotoolkit/open_model_zoo/raw/master/data/dataset_classes/coco_91cl_bkgr.txt"; + hash = "sha256-5Cj2vEiWR8Z9d2xBmVoLZuNRv4UOuxHSGZQWTJorXUQ="; + }; +in +python.pkgs.buildPythonApplication rec { + pname = "frigate"; + inherit version; + format = "other"; + + inherit src; + + patches = [ + (fetchpatch { + # numpy 1.24 compat + url = "https://github.com/blakeblackshear/frigate/commit/cb73d0cd392990448811c7212bc5f09be411fc69.patch"; + hash = "sha256-Spt7eRosmTN8zyJ2uVme5HPVy2TKgBtvbQ6tp6PaNac="; + }) + ]; + + postPatch = '' + echo 'VERSION = "${version}"' > frigate/version.py + + substituteInPlace frigate/app.py \ + --replace "Router(migrate_db)" 'Router(migrate_db, "${placeholder "out"}/share/frigate/migrations")' + + substituteInPlace frigate/const.py \ + --replace "/media/frigate" "/var/lib/frigate" \ + --replace "/tmp/cache" "/var/cache/frigate" + + substituteInPlace frigate/detectors/detector_config.py \ + --replace "/labelmap.txt" "${placeholder "out"}/share/frigate/labelmap.txt" + + substituteInPlace frigate/detectors/plugins/edgetpu_tfl.py \ + --replace "/edgetpu_model.tflite" "${tflite_edgetpu_model}" + + substituteInPlace frigate/detectors/plugins/cpu_tfl.py \ + --replace "/cpu_model.tflite" "${tflite_cpu_model}" + + substituteInPlace frigate/ffmpeg_presets.py --replace \ + '"-timeout" if os.path.exists(BTBN_PATH) else "-stimeout"' \ + '"-timeout"' + ''; + + dontBuild = true; + + propagatedBuildInputs = with python.pkgs; [ + # requirements.txt + scikit-build + # requirements-wheel.txt + click + flask + imutils + matplotlib + numpy + opencv + openvino + paho-mqtt + peewee + peewee-migrate + psutil + pydantic + pyyaml + requests + scipy + setproctitle + tensorflow + tzlocal + ws4py + zeroconf + ]; + + installPhase = '' + runHook preInstall + + mkdir -p $out/${python.sitePackages}/frigate + cp -R frigate/* $out/${python.sitePackages}/frigate/ + + mkdir -p $out/share/frigate + cp -R {migrations,labelmap.txt} $out/share/frigate/ + + cp --no-preserve=mode ${openvino_model} $out/share/frigate/coco_91cl_bkgr.txt + sed -i 's/truck/car/g' $out/share/frigate/coco_91cl_bkgr.txt + + runHook postInstall + ''; + + checkInputs = with python.pkgs; [ + pytestCheckHook + ]; + + passthru = { + web = frigate-web; + inherit python; + pythonPath =(python.pkgs.makePythonPath propagatedBuildInputs) + ":${frigate}/${python.sitePackages}"; + tests = { + inherit (nixosTests) frigate; + }; + }; + + meta = with lib; { + changelog = "https://github.com/blakeblackshear/frigate/releases/tag/v${version}"; + description = "NVR with realtime local object detection for IP cameras"; + longDescription = '' + A complete and local NVR designed for Home Assistant with AI + object detection. Uses OpenCV and Tensorflow to perform realtime + object detection locally for IP cameras. + ''; + homepage = "https://github.com/blakeblackshear/frigate"; + license = licenses.mit; + maintainers = with maintainers; [ hexa ]; + }; +} diff --git a/pkgs/applications/video/frigate/web.nix b/pkgs/applications/video/frigate/web.nix new file mode 100644 index 0000000000000..08b9cef19ee76 --- /dev/null +++ b/pkgs/applications/video/frigate/web.nix @@ -0,0 +1,26 @@ +{ buildNpmPackage +, src +, version +}: + +buildNpmPackage { + pname = "frigate-web"; + inherit version src; + + sourceRoot = "source/web"; + + postPatch = '' + substituteInPlace package.json \ + --replace "--base=/BASE_PATH/" "" + + substituteInPlace src/routes/Storage.jsx \ + --replace "/media/frigate" "/var/lib/frigate" \ + --replace "/tmp/cache" "/var/cache/frigate" + ''; + + npmDepsHash = "sha256-fvRxpQjSEzd2CnoEOVgQcB6MJJ4dcjN8bOaacHjCdwU="; + + installPhase = '' + cp -rv dist/ $out + ''; +} diff --git a/pkgs/servers/http/nginx/modules.nix b/pkgs/servers/http/nginx/modules.nix index aa070df188c4c..14e9e97deb0f7 100644 --- a/pkgs/servers/http/nginx/modules.nix +++ b/pkgs/servers/http/nginx/modules.nix @@ -662,14 +662,15 @@ let self = { }; }; - secure-token = { + secure-token = rec { name = "secure-token"; + version = "1.5"; src = fetchFromGitHub { name = "secure-token"; owner = "kaltura"; repo = "nginx-secure-token-module"; - rev = "95bdc0d1aca06ea7fe42555f71e65910bd74914d"; - sha256 = "19wzck1xzq4kz7nyabcwzlank1k7wi7w2wn2c1mwz374c79g8ggp"; + rev = "refs/tags/${version}"; + hash = "sha256-qYTjGS9pykRqMFmNls52YKxEdXYhHw+18YC2zzdjEpU="; }; inputs = [ openssl ]; @@ -980,8 +981,8 @@ let self = { name = "vod"; owner = "kaltura"; repo = "nginx-vod-module"; - rev = "1.29"; - sha256 = "1z0ka0cwqbgh3fv2d5yva395sf90626rdzx7lyfrgs89gy4h9nrr"; + rev = "1.31"; + hash = "sha256-ZpeO8QWQ+fGkz08u/zFOq7vj4aHcodzSHNrc1SgGUyc="; }; inputs = [ ffmpeg fdk_aac openssl libxml2 libiconv ]; diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 1f38640fd8f61..d97eb416919ce 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -7720,6 +7720,8 @@ with pkgs; inherit (darwin.apple_sdk.frameworks) AppKit CoreFoundation DiskArbitration Foundation IOKit; }; + frigate = callPackage ../applications/video/frigate { }; + frostwire = callPackage ../applications/networking/p2p/frostwire { }; frostwire-bin = callPackage ../applications/networking/p2p/frostwire/frostwire-bin.nix { }; |