about summary refs log tree commit diff
path: root/nixos/modules/services/video
diff options
context:
space:
mode:
authorMartin Weinelt <hexa@darmstadt.ccc.de>2023-03-30 13:04:55 +0200
committerMartin Weinelt <hexa@darmstadt.ccc.de>2023-05-22 16:29:54 +0200
commitf11d33afb7cc37b90a3b350621522e80da000dd8 (patch)
treeb0b8e89e73ecd566c6d70b21087aa7956bc83697 /nixos/modules/services/video
parent76f9a4b6170eabf374a44641b88be5729d629509 (diff)
nixos/frigate: init
Diffstat (limited to 'nixos/modules/services/video')
-rw-r--r--nixos/modules/services/video/frigate.nix368
1 files changed, 368 insertions, 0 deletions
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"
+        ];
+      };
+    };
+  };
+}