diff --git a/default.nix b/default.nix index ea2ed8f..eaaa31b 100644 --- a/default.nix +++ b/default.nix @@ -6,9 +6,14 @@ self: super: { packageOverrides = python-self: python-super: { pytapo = python-self.callPackage ./pytapo { }; brother-ql = python-self.callPackage ./brother-ql { }; + onvif-zeep = python-self.callPackage ./onvif-zeep { }; + motmetrics = python-self.callPackage ./motmetrics { }; + norfair = python-self.callPackage ./norfair { }; + pydantic = python-super.pydantic_1; }; }; + frigate = super.callPackage ./frigate { }; #gotosocial = super.callPackage ./gotosocial { }; pulse-secure = super.callPackage ./pulse-secure { }; studio-link = super.callPackage ./studio-link { }; diff --git a/frigate/default.nix b/frigate/default.nix new file mode 100644 index 0000000..146f5e2 --- /dev/null +++ b/frigate/default.nix @@ -0,0 +1,170 @@ +{ lib +, callPackage +, python3 +, fetchFromGitHub +, fetchurl +, frigate +, nixosTests +}: + +let + version = "0.13.1"; + + src = fetchFromGitHub { + #name = "frigate-${version}-source"; + owner = "blakeblackshear"; + repo = "frigate"; + rev = "refs/tags/v${version}"; + hash = "sha256-2J7DhnYDX9ubbsk0qhji/vIKDouy9IqQztzbdPj2kxo="; + }; + + frigate-web = callPackage ./web.nix { + inherit version src; + }; + + python = python3.override { + packageOverrides = self: super: { + + versioningit = super.versioningit.overridePythonAttrs { + # checkPhase requires pydantic>=2 + doCheck = false; + }; + onvif-zeep = self.callPackage ../onvif-zeep { }; + norfair = self.callPackage ../norfair { }; + }; + }; + + # Tensorflow Lite models + # https://github.com/blakeblackshear/frigate/blob/v0.13.0/docker/main/Dockerfile#L96-L97 + 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.13.0/docker/main/Dockerfile#L101 + 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; + + 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" \ + --replace "/config" "/var/lib/frigate" \ + --replace "{CONFIG_DIR}/model_cache" "/var/cache/frigate/model_cache" + + substituteInPlace frigate/http.py \ + --replace "/opt/frigate" "${placeholder "out"}/${python.sitePackages}" + + substituteInPlace frigate/output.py \ + --replace "/opt/frigate" "${placeholder "out"}/${python.sitePackages}" + + substituteInPlace frigate/detectors/detector_config.py \ + --replace "/labelmap.txt" "${placeholder "out"}/share/frigate/labelmap.txt" + + substituteInPlace frigate/config.py \ + --replace "/cpu_model.tflite" "${tflite_cpu_model}" \ + --replace "/edgetpu_model.tflite" "${tflite_edgetpu_model}" + + substituteInPlace frigate/test/test_config.py \ + --replace "(MODEL_CACHE_DIR" "('/build/model_cache'" \ + --replace "/config/model_cache" "/build/model_cache" + ''; + + dontBuild = true; + + propagatedBuildInputs = with python.pkgs; [ + # docker/main/requirements.txt + scikit-build + # docker/main/requirements-wheel.txt + click + flask + imutils + matplotlib + norfair + numpy + onvif-zeep + opencv4 + openvino + paho-mqtt + peewee + peewee-migrate + psutil + py3nvml + pydantic + pytz + pyyaml + requests + ruamel-yaml + scipy + setproctitle + tensorflow + tzlocal + unidecode + ws4py + ]; + + 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 + ''; + + nativeCheckInputs = with python.pkgs; [ + pytestCheckHook + ]; + + disabledTests = [ + # Test needs network access + "test_plus_labelmap" + ]; + + 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/frigate/web.nix b/frigate/web.nix new file mode 100644 index 0000000..da6d9fe --- /dev/null +++ b/frigate/web.nix @@ -0,0 +1,26 @@ +{ buildNpmPackage +, src +, version +}: + +buildNpmPackage { + pname = "frigate-web"; + inherit version src; + + sourceRoot = "${src.name}/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-+36quezGArqIM9dM+UihwcIgmE3EVmJQThuicLgDW4A="; + + installPhase = '' + cp -rv dist/ $out + ''; +} diff --git a/modules/default.nix b/modules/default.nix index 017b18b..ec1cef4 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,5 +2,6 @@ imports = [ #./gotosocial.nix ./mealie.nix + #./frigate.nix ]; } diff --git a/modules/frigate.nix b/modules/frigate.nix new file mode 100644 index 0000000..ab4ec13 --- /dev/null +++ b/modules/frigate.nix @@ -0,0 +1,438 @@ +{ config +, lib +, pkgs +, ... +}: + +let + inherit (lib) + literalExpression + mkDefault + mdDoc + mkEnableOption + mkPackageOption + 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 = mkPackageOption pkgs "frigate" { }; + + 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; + mapHashBucketSize = mkDefault 128; + 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" = { }; + }; + }; + proxyCachePath."frigate" = { + enable = true; + keysZoneSize = "10m"; + keysZoneName = "api_cache"; + maxSize = "10m"; + inactive = "1m"; + levels = "1:2"; + }; + # 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/"; + extraConfig = '' + proxy_cache api_cache; + proxy_cache_lock on; + proxy_cache_use_stale updating; + proxy_cache_valid 200 5s; + proxy_cache_bypass $http_x_cache_bypass; + proxy_no_cache $should_not_cache; + add_header X-Cache-Status $upstream_cache_status; + + location /api/vod/ { + proxy_pass http://frigate-api/vod/; + proxy_cache off; + } + + location /api/stats { + access_log off; + rewrite ^/api/(.*)$ $1 break; + proxy_pass http://frigate-api; + } + + location /api/version { + access_log off; + rewrite ^/api/(.*)$ $1 break; + proxy_pass http://frigate-api; + } + ''; + }; + "~* /api/.*\.(jpg|jpeg|png)$" = { + proxyPass = "http://frigate-api"; + extraConfig = '' + rewrite ^/api/(.*)$ $1 break; + ''; + }; + "/vod/" = { + extraConfig = '' + aio threads; + vod hls; + + secure_token $args; + secure_token_types application/vnd.apple.mpegurl; + + 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; + }; + # frigate lovelace card uses this path + "/live/mse/api/ws" = { + proxyPass = "http://frigate-go2rtc/api/ws"; + proxyWebsockets = true; + extraConfig = '' + limit_except GET { + deny all; + } + ''; + }; + "/live/webrtc/" = { + proxyPass = "http://frigate-go2rtc/"; + proxyWebsockets = true; + }; + "/live/webrtc/api/ws" = { + proxyPass = "http://frigate-go2rtc/api/ws"; + proxyWebsockets = true; + extraConfig = '' + limit_except GET { + deny all; + } + ''; + }; + # pass through go2rtc player + "/live/webrtc/webrtc.html" = { + proxyPass = "http://frigate-go2rtc/webrtc.html"; + proxyWebsockets = true; + extraConfig = '' + limit_except GET { + deny all; + } + ''; + }; + "/api/go2rtc/api" = { + proxyPass = "http://frigate-go2rtc/api"; + proxyWebsockets = true; + extraConfig = '' + limit_except GET { + deny all; + } + ''; + }; + # integrationn uses this to add webrtc candidate + "/api/go2rtc/webrtc" = { + proxyPass = "http://frigate-go2rtc/api/webrtc"; + proxyWebsockets = true; + extraConfig = '' + limit_except GET { + deny all; + } + ''; + }; + "/cache/" = { + alias = "/var/cache/frigate/"; + }; + "/clips/" = { + root = "/var/lib/frigate"; + extraConfig = '' + types { + video/mp4 mp4; + image/jpeg jpg; + } + + autoindex on; + ''; + }; + "/recordings/" = { + root = "/var/lib/frigate"; + extraConfig = '' + 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 '' ''; + 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; + } + } + } + ''; + appendHttpConfig = '' + map $sent_http_content_type $should_not_cache { + 'application/json' 0; + default 1; + } + ''; + }; + + systemd.services.nginx.serviceConfig.SupplementaryGroups = [ + "frigate" + ]; + + users.users.frigate = { + isSystemUser = true; + group = "frigate"; + }; + users.groups.frigate = { }; + + 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"; + Restart = "on-failure"; + + User = "frigate"; + Group = "frigate"; + + UMask = "0027"; + + StateDirectory = "frigate"; + StateDirectoryMode = "0750"; + + # Caches + PrivateTmp = true; + CacheDirectory = "frigate"; + CacheDirectoryMode = "0750"; + + BindPaths = [ + "/migrations:${cfg.package}/share/frigate/migrations:ro" + ]; + }; + }; + }; +} diff --git a/motmetrics/default.nix b/motmetrics/default.nix new file mode 100644 index 0000000..36fa2d9 --- /dev/null +++ b/motmetrics/default.nix @@ -0,0 +1,58 @@ +{ lib +, buildPythonPackage +, fetchFromGitHub + +# build-system +, setuptools + +# dependencies +, numpy +, pandas +, scipy +, xmltodict + +# tests +, pytestCheckHook +, pytest-benchmark +}: + +buildPythonPackage rec { + pname = "motmetrics"; + version = "1.4.0-unstable-20240130"; + pyproject = true; + + src = fetchFromGitHub { + owner = "cheind"; + repo = "py-motmetrics"; + # latest release is not compatible with pandas 2.0 + rev = "7210fcce0be1b76c96a62f6fe4ddbc90d944eacb"; + hash = "sha256-7LKLHXWgW4QpivAgzvWl6qEG0auVvpiZ6bfDViCKsFY="; + }; + + nativeBuildInputs = [ + setuptools + ]; + + propagatedBuildInputs = [ + numpy + pandas + scipy + xmltodict + ]; + + nativeCheckInputs = [ + pytestCheckHook + pytest-benchmark + ]; + + pythonImportsCheck = [ + "motmetrics" + ]; + + meta = with lib; { + description = "Bar_chart: Benchmark multiple object trackers (MOT) in Python"; + homepage = "https://github.com/cheind/py-motmetrics"; + license = licenses.mit; + maintainers = with maintainers; [ ]; + }; +} diff --git a/norfair/default.nix b/norfair/default.nix new file mode 100644 index 0000000..921bc9b --- /dev/null +++ b/norfair/default.nix @@ -0,0 +1,69 @@ +{ lib +, buildPythonPackage +, fetchFromGitHub +, poetry-core +, filterpy +, importlib-metadata +, numpy +, rich +, scipy +, opencv4 +, pytestCheckHook +, pythonRelaxDepsHook +, python3 +}: + +buildPythonPackage rec { + pname = "norfair"; + version = "2.2.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "tryolabs"; + repo = "norfair"; + rev = "v${version}"; + hash = "sha256-aKB5TYSLW7FOXIy9u2hK7px6eEmIQdKPrhChKaU1uYs="; + }; + + nativeBuildInputs = [ + poetry-core + pythonRelaxDepsHook + ]; + + pythonRelaxDeps = [ + "rich" + ]; + + propagatedBuildInputs = [ + filterpy + importlib-metadata + numpy + rich + scipy + ]; + + passthru.optional-dependencies = { + metrics = [ + python3.pkgs.motmetrics + ]; + video = [ + opencv4 + ]; + }; + + nativeCheckInputs = [ + pytestCheckHook + ]; + + pythonImportsCheck = [ + "norfair" + ]; + + meta = with lib; { + description = "Lightweight Python library for adding real-time multi-object tracking to any detector"; + changelog = "https://github.com/tryolabs/norfair/releases/tag/v${version}"; + homepage = "https://github.com/tryolabs/norfair"; + license = licenses.bsd3; + maintainers = with maintainers; [ fleaz ]; + }; +} diff --git a/onvif-zeep/default.nix b/onvif-zeep/default.nix new file mode 100644 index 0000000..c1d60fd --- /dev/null +++ b/onvif-zeep/default.nix @@ -0,0 +1,40 @@ +{ lib +, buildPythonPackage +, fetchPypi +, setuptools +, zeep +}: + +buildPythonPackage rec { + pname = "onvif-zeep"; + version = "0.2.12"; + pyproject = true; + + src = fetchPypi { + pname = "onvif_zeep"; + inherit version; + hash = "sha256-qou8Aqc+qlCJSwwY45+o0xilg6ZkxlvzWzyAKdHEC0k="; + }; + + nativeBuildInputs = [ + setuptools + ]; + + propagatedBuildInputs = [ + zeep + ]; + + pythonImportsCheck = [ + "onvif" + ]; + + # Tests require hardware + doCheck = false; + + meta = with lib; { + description = "Python Client for ONVIF Camera"; + homepage = "https://github.com/quatanium/python-onvif"; + license = licenses.mit; + maintainers = with maintainers; [ fleaz ]; + }; +}