at master 23 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 inherit (lib) 10 any 11 attrValues 12 converge 13 elem 14 filterAttrsRecursive 15 hasPrefix 16 literalExpression 17 makeLibraryPath 18 mkDefault 19 mkEnableOption 20 mkPackageOption 21 mkIf 22 mkOption 23 optionalAttrs 24 optionals 25 types 26 ; 27 28 cfg = config.services.frigate; 29 30 format = pkgs.formats.yaml { }; 31 32 filteredConfig = converge (filterAttrsRecursive (_: v: !elem v [ null ])) cfg.settings; 33 34 configFileUnchecked = format.generate "frigate.yaml" filteredConfig; 35 configFileChecked = 36 pkgs.runCommand "frigate-config" 37 { 38 preferLocalBuilds = true; 39 } 40 '' 41 function error() { 42 cat << 'HEREDOC' 43 44 Note that not all configurations can be reliably checked in the 45 build sandbox. 46 47 This check can be disabled using `services.frigate.checkConfig`. 48 HEREDOC 49 50 exit 1 51 } 52 53 cp ${configFileUnchecked} $out 54 export CONFIG_FILE=$out 55 export PYTHONPATH=${cfg.package.pythonPath} 56 ${cfg.package.python.interpreter} -m frigate --validate-config || error 57 ''; 58 configFile = if cfg.checkConfig then configFileChecked else configFileUnchecked; 59 60 cameraFormat = 61 with types; 62 submodule { 63 freeformType = format.type; 64 options = { 65 ffmpeg = { 66 inputs = mkOption { 67 description = '' 68 List of inputs for this camera. 69 ''; 70 type = listOf (submodule { 71 freeformType = format.type; 72 options = { 73 path = mkOption { 74 type = str; 75 example = "rtsp://192.0.2.1:554/rtsp"; 76 description = '' 77 Stream URL 78 ''; 79 }; 80 roles = mkOption { 81 type = listOf (enum [ 82 "audio" 83 "detect" 84 "record" 85 ]); 86 example = [ 87 "detect" 88 "record" 89 ]; 90 description = '' 91 List of roles for this stream 92 ''; 93 }; 94 }; 95 }); 96 }; 97 }; 98 }; 99 }; 100 101 # auth_request.conf 102 nginxAuthRequest = '' 103 # Send a subrequest to verify if the user is authenticated and has permission to access the resource. 104 auth_request /auth; 105 106 # Save the upstream metadata response headers from the auth request to variables 107 auth_request_set $user $upstream_http_remote_user; 108 auth_request_set $role $upstream_http_remote_role; 109 auth_request_set $groups $upstream_http_remote_groups; 110 auth_request_set $name $upstream_http_remote_name; 111 auth_request_set $email $upstream_http_remote_email; 112 113 # Inject the metadata response headers from the variables into the request made to the backend. 114 proxy_set_header Remote-User $user; 115 proxy_set_header Remote-Role $role; 116 proxy_set_header Remote-Groups $groups; 117 proxy_set_header Remote-Email $email; 118 proxy_set_header Remote-Name $name; 119 120 # Refresh the cookie as needed 121 auth_request_set $auth_cookie $upstream_http_set_cookie; 122 add_header Set-Cookie $auth_cookie; 123 124 # Pass the location header back up if it exists 125 auth_request_set $redirection_url $upstream_http_location; 126 add_header Location $redirection_url; 127 ''; 128 129 nginxProxySettings = '' 130 # Basic Proxy Configuration 131 client_body_buffer_size 128k; 132 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. 133 proxy_redirect http:// $scheme://; 134 proxy_cache_bypass $cookie_session; 135 proxy_no_cache $cookie_session; 136 proxy_buffers 64 256k; 137 138 # Advanced Proxy Configuration 139 send_timeout 5m; 140 proxy_read_timeout 360; 141 proxy_send_timeout 360; 142 proxy_connect_timeout 360; 143 ''; 144 145 # Discover configured detectors for acceleration support 146 detectors = attrValues cfg.settings.detectors or { }; 147 withCoralUSB = any (d: d.type == "edgetpu" && hasPrefix "usb" d.device or "") detectors; 148 withCoralPCI = any (d: d.type == "edgetpu" && hasPrefix "pci" d.device or "") detectors; 149 withCoral = withCoralPCI || withCoralUSB; 150in 151 152{ 153 meta.buildDocsInSandbox = false; 154 155 options.services.frigate = with types; { 156 enable = mkEnableOption "Frigate NVR"; 157 158 package = mkPackageOption pkgs "frigate" { }; 159 160 hostname = mkOption { 161 type = str; 162 example = "frigate.exampe.com"; 163 description = '' 164 Hostname of the nginx vhost to configure. 165 166 Only nginx is supported by upstream for direct reverse proxying. 167 ''; 168 }; 169 170 vaapiDriver = mkOption { 171 type = nullOr (enum [ 172 "i965" 173 "iHD" 174 "nouveau" 175 "vdpau" 176 "nvidia" 177 "radeonsi" 178 ]); 179 default = null; 180 example = "radeonsi"; 181 description = '' 182 Force usage of a particular VA-API driver for video acceleration. Use together with `settings.ffmpeg.hwaccel_args`. 183 184 Setting this *is not required* for VA-API to work, but it can help steer VA-API towards the correct card if you have multiple. 185 186 :::{.note} 187 For VA-API to work you must enable {option}`hardware.graphics.enable` (sufficient for AMDGPU) and pass for example 188 `pkgs.intel-media-driver` (required for Intel 5th Gen. and newer) into {option}`hardware.graphics.extraPackages`. 189 ::: 190 191 See also: 192 193 - <https://docs.frigate.video/configuration/hardware_acceleration> 194 - <https://docs.frigate.video/configuration/ffmpeg_presets#hwaccel-presets> 195 ''; 196 }; 197 198 checkConfig = mkOption { 199 type = bool; 200 default = 201 pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform 202 && (!pkgs.stdenv.hostPlatform.isAarch64); 203 defaultText = literalExpression '' 204 pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform && !(pkgs.stdenv.hostPlaform.isAarch64) 205 ''; 206 description = '' 207 Whether to check the configuration at build time. 208 ''; 209 }; 210 211 settings = mkOption { 212 type = submodule { 213 freeformType = format.type; 214 options = { 215 cameras = mkOption { 216 type = attrsOf cameraFormat; 217 description = '' 218 Attribute set of cameras configurations. 219 220 <https://docs.frigate.video/configuration/cameras> 221 ''; 222 }; 223 224 database = { 225 path = mkOption { 226 type = path; 227 default = "/var/lib/frigate/frigate.db"; 228 description = '' 229 Path to the SQLite database used 230 ''; 231 }; 232 }; 233 234 ffmpeg = { 235 path = mkOption { 236 type = coercedTo package toString str; 237 default = pkgs.ffmpeg-headless; 238 example = literalExpression "pkgs.ffmpeg-full"; 239 description = '' 240 Package providing the ffmpeg and ffprobe executables below the bin/ directory. 241 ''; 242 }; 243 }; 244 245 mqtt = { 246 enabled = mkEnableOption "MQTT support"; 247 248 host = mkOption { 249 type = nullOr str; 250 default = null; 251 example = "mqtt.example.com"; 252 description = '' 253 MQTT server hostname 254 ''; 255 }; 256 }; 257 }; 258 }; 259 default = { }; 260 description = '' 261 Frigate configuration as a nix attribute set. 262 263 See the project documentation for how to configure frigate. 264 - [Creating a config file](https://docs.frigate.video/guides/getting_started) 265 - [Configuration reference](https://docs.frigate.video/configuration/index) 266 ''; 267 }; 268 }; 269 270 config = mkIf cfg.enable { 271 services.nginx = { 272 enable = true; 273 additionalModules = with pkgs.nginxModules; [ 274 develkit 275 rtmp 276 secure-token 277 set-misc 278 vod 279 ]; 280 recommendedGzipSettings = mkDefault true; 281 mapHashBucketSize = mkDefault 128; 282 upstreams = { 283 frigate-api.servers = { 284 "127.0.0.1:5001" = { }; 285 }; 286 frigate-mqtt-ws.servers = { 287 "127.0.0.1:5002" = { }; 288 }; 289 frigate-jsmpeg.servers = { 290 "127.0.0.1:8082" = { }; 291 }; 292 frigate-go2rtc.servers = { 293 "127.0.0.1:1984" = { }; 294 }; 295 }; 296 proxyCachePath."frigate" = { 297 enable = true; 298 keysZoneSize = "10m"; 299 keysZoneName = "frigate_api_cache"; 300 maxSize = "10m"; 301 inactive = "1m"; 302 levels = "1:2"; 303 }; 304 # Based on https://github.com/blakeblackshear/frigate/blob/v0.13.1/docker/main/rootfs/usr/local/nginx/conf/nginx.conf 305 virtualHosts."${cfg.hostname}" = { 306 locations = { 307 # auth_location.conf 308 "/auth" = { 309 proxyPass = "http://frigate-api/auth"; 310 recommendedProxySettings = true; 311 extraConfig = '' 312 internal; 313 314 # Strip all request headers 315 proxy_pass_request_headers off; 316 317 # Pass info about the request 318 proxy_set_header X-Original-Method $request_method; 319 proxy_set_header X-Original-URL $scheme://$http_host$request_uri; 320 proxy_set_header X-Server-Port $server_port; 321 proxy_set_header Content-Length ""; 322 323 # Pass along auth related info 324 proxy_set_header Authorization $http_authorization; 325 proxy_set_header Cookie $http_cookie; 326 proxy_set_header X-CSRF-TOKEN "1"; 327 328 # Header used to validate reverse proxy trust 329 proxy_set_header X-Proxy-Secret $http_x_proxy_secret; 330 331 # Pass headers for common auth proxies 332 proxy_set_header Remote-User $http_remote_user; 333 proxy_set_header Remote-Groups $http_remote_groups; 334 proxy_set_header Remote-Email $http_remote_email; 335 proxy_set_header Remote-Name $http_remote_name; 336 proxy_set_header X-Forwarded-User $http_x_forwarded_user; 337 proxy_set_header X-Forwarded-Groups $http_x_forwarded_groups; 338 proxy_set_header X-Forwarded-Email $http_x_forwarded_email; 339 proxy_set_header X-Forwarded-Preferred-Username $http_x_forwarded_preferred_username; 340 proxy_set_header X-authentik-username $http_x_authentik_username; 341 proxy_set_header X-authentik-groups $http_x_authentik_groups; 342 proxy_set_header X-authentik-email $http_x_authentik_email; 343 proxy_set_header X-authentik-name $http_x_authentik_name; 344 proxy_set_header X-authentik-uid $http_x_authentik_uid; 345 346 ${nginxProxySettings} 347 ''; 348 }; 349 "/vod/" = { 350 extraConfig = nginxAuthRequest + '' 351 aio threads; 352 vod hls; 353 354 secure_token $args; 355 secure_token_types application/vnd.apple.mpegurl; 356 357 add_header Cache-Control "no-store"; 358 expires off; 359 360 keepalive_disable safari; 361 362 # vod module returns 502 for non-existent media 363 # https://github.com/kaltura/nginx-vod-module/issues/468 364 error_page 502 =404 /vod-not-found; 365 ''; 366 }; 367 "/vod-not-found" = { 368 return = 404; 369 }; 370 "/stream/" = { 371 alias = "/var/cache/frigate/stream/"; 372 extraConfig = nginxAuthRequest + '' 373 add_header Cache-Control "no-store"; 374 expires off; 375 376 types { 377 application/dash+xml mpd; 378 application/vnd.apple.mpegurl m3u8; 379 video/mp2t ts; 380 image/jpeg jpg; 381 } 382 ''; 383 }; 384 "/clips/" = { 385 root = "/var/lib/frigate"; 386 extraConfig = nginxAuthRequest + '' 387 types { 388 video/mp4 mp4; 389 image/jpeg jpg; 390 } 391 392 expires 7d; 393 add_header Cache-Control "public"; 394 autoindex on; 395 ''; 396 }; 397 "/cache/" = { 398 alias = "/var/cache/frigate/"; 399 extraConfig = '' 400 internal; 401 ''; 402 }; 403 "/recordings/" = { 404 root = "/var/lib/frigate"; 405 extraConfig = nginxAuthRequest + '' 406 types { 407 video/mp4 mp4; 408 } 409 410 autoindex on; 411 autoindex_format json; 412 ''; 413 }; 414 "/exports/" = { 415 root = "/var/lib/frigate"; 416 extraConfig = nginxAuthRequest + '' 417 types { 418 video/mp4 mp4; 419 } 420 421 autoindex on; 422 autoindex_format json; 423 ''; 424 }; 425 "/ws" = { 426 proxyPass = "http://frigate-mqtt-ws/"; 427 recommendedProxySettings = true; 428 proxyWebsockets = true; 429 extraConfig = nginxAuthRequest + nginxProxySettings; 430 }; 431 "/live/jsmpeg" = { 432 proxyPass = "http://frigate-jsmpeg/"; 433 recommendedProxySettings = true; 434 proxyWebsockets = true; 435 extraConfig = nginxAuthRequest + nginxProxySettings; 436 }; 437 # frigate lovelace card uses this path 438 "/live/mse/api/ws" = { 439 proxyPass = "http://frigate-go2rtc/api/ws"; 440 proxyWebsockets = true; 441 recommendedProxySettings = true; 442 extraConfig = 443 nginxAuthRequest 444 + nginxProxySettings 445 + '' 446 limit_except GET { 447 deny all; 448 } 449 ''; 450 }; 451 "/live/webrtc/api/ws" = { 452 proxyPass = "http://frigate-go2rtc/api/ws"; 453 proxyWebsockets = true; 454 recommendedProxySettings = true; 455 extraConfig = 456 nginxAuthRequest 457 + nginxProxySettings 458 + '' 459 limit_except GET { 460 deny all; 461 } 462 ''; 463 }; 464 # pass through go2rtc player 465 "/live/webrtc/webrtc.html" = { 466 proxyPass = "http://frigate-go2rtc/webrtc.html"; 467 recommendedProxySettings = true; 468 extraConfig = 469 nginxAuthRequest 470 + nginxProxySettings 471 + '' 472 limit_except GET { 473 deny all; 474 } 475 ''; 476 }; 477 # frontend uses this to fetch the version 478 "/api/go2rtc/api" = { 479 proxyPass = "http://frigate-go2rtc/api"; 480 recommendedProxySettings = true; 481 extraConfig = 482 nginxAuthRequest 483 + nginxProxySettings 484 + '' 485 limit_except GET { 486 deny all; 487 } 488 ''; 489 }; 490 # integrationn uses this to add webrtc candidate 491 "/api/go2rtc/webrtc" = { 492 proxyPass = "http://frigate-go2rtc/api/webrtc"; 493 proxyWebsockets = true; 494 recommendedProxySettings = true; 495 extraConfig = 496 nginxAuthRequest 497 + nginxProxySettings 498 + '' 499 limit_except GET { 500 deny all; 501 } 502 ''; 503 }; 504 "~* /api/.*\\.(jpg|jpeg|png|webp|gif)$" = { 505 proxyPass = "http://frigate-api"; 506 recommendedProxySettings = true; 507 extraConfig = 508 nginxAuthRequest 509 + nginxProxySettings 510 + '' 511 rewrite ^/api/(.*)$ /$1 break; 512 ''; 513 }; 514 "/api/" = { 515 proxyPass = "http://frigate-api/"; 516 recommendedProxySettings = true; 517 extraConfig = 518 nginxAuthRequest 519 + nginxProxySettings 520 + '' 521 add_header Cache-Control "no-store"; 522 expires off; 523 524 proxy_cache frigate_api_cache; 525 proxy_cache_lock on; 526 proxy_cache_use_stale updating; 527 proxy_cache_valid 200 5s; 528 proxy_cache_bypass $http_x_cache_bypass; 529 proxy_no_cache $should_not_cache; 530 add_header X-Cache-Status $upstream_cache_status; 531 532 location /api/vod/ { 533 ${nginxAuthRequest} 534 proxy_pass http://frigate-api/vod/; 535 proxy_cache off; 536 add_header Cache-Control "no-store"; 537 ${nginxProxySettings} 538 } 539 540 location /api/login { 541 auth_request off; 542 rewrite ^/api(/.*)$ $1 break; 543 proxy_pass http://frigate-api; 544 ${nginxProxySettings} 545 } 546 547 location /api/stats { 548 ${nginxAuthRequest} 549 access_log off; 550 rewrite ^/api(/.*)$ $1 break; 551 add_header Cache-Control "no-store"; 552 proxy_pass http://frigate-api; 553 ${nginxProxySettings} 554 } 555 556 location /api/version { 557 ${nginxAuthRequest} 558 access_log off; 559 rewrite ^/api(/.*)$ $1 break; 560 add_header Cache-Control "no-store"; 561 proxy_pass http://frigate-api; 562 ${nginxProxySettings} 563 } 564 ''; 565 }; 566 "/assets/" = { 567 root = cfg.package.web; 568 extraConfig = '' 569 access_log off; 570 expires 1y; 571 add_header Cache-Control "public"; 572 ''; 573 }; 574 "/locales/" = { 575 root = cfg.package.web; 576 extraConfig = '' 577 access_log off; 578 add_header Cache-Control "public"; 579 ''; 580 }; 581 "~ ^/.*-([A-Za-z0-9]+)\.webmanifest$" = { 582 root = cfg.package.web; 583 extraConfig = '' 584 access_log off; 585 expires 1y; 586 add_header Cache-Control "public"; 587 default_type application/json; 588 proxy_set_header Accept-Encoding ""; 589 ''; 590 }; 591 "/" = { 592 root = cfg.package.web; 593 tryFiles = "$uri $uri.html $uri/ /index.html"; 594 extraConfig = '' 595 add_header Cache-Control "no-store"; 596 expires off; 597 ''; 598 }; 599 }; 600 extraConfig = '' 601 # Frigate wants to connect on 127.0.0.1:5000 for unauthenticated requests 602 # https://github.com/NixOS/nixpkgs/issues/370349 603 listen 127.0.0.1:5000; 604 605 # vod settings 606 vod_base_url ""; 607 vod_segments_base_url ""; 608 vod_mode mapped; 609 vod_max_mapping_response_size 1m; 610 vod_upstream_location /api; 611 vod_align_segments_to_key_frames on; 612 vod_manifest_segment_durations_mode accurate; 613 vod_ignore_edit_list on; 614 vod_segment_duration 10000; 615 vod_hls_mpegts_align_frames off; 616 vod_hls_mpegts_interleave_frames on; 617 618 # file handle caching / aio 619 open_file_cache max=1000 inactive=5m; 620 open_file_cache_valid 2m; 621 open_file_cache_min_uses 1; 622 open_file_cache_errors on; 623 aio on; 624 625 # file upload size 626 client_max_body_size 20M; 627 628 # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool 629 vod_open_file_thread_pool default; 630 631 # vod caches 632 vod_metadata_cache metadata_cache 512m; 633 vod_mapping_cache mapping_cache 5m 10m; 634 635 # gzip manifest 636 gzip_types application/vnd.apple.mpegurl; 637 ''; 638 }; 639 appendConfig = '' 640 rtmp { 641 server { 642 listen 1935; 643 chunk_size 4096; 644 allow publish 127.0.0.1; 645 deny publish all; 646 allow play all; 647 application live { 648 live on; 649 record off; 650 meta copy; 651 } 652 } 653 } 654 ''; 655 appendHttpConfig = '' 656 map $sent_http_content_type $should_not_cache { 657 'application/json' 0; 658 default 1; 659 } 660 ''; 661 }; 662 663 systemd.services.nginx.serviceConfig.SupplementaryGroups = [ 664 "frigate" 665 ]; 666 667 hardware.coral = { 668 usb.enable = mkDefault withCoralUSB; 669 pcie.enable = mkDefault withCoralPCI; 670 }; 671 672 users.users.frigate = { 673 isSystemUser = true; 674 group = "frigate"; 675 }; 676 users.groups.frigate = { }; 677 678 systemd.services.frigate = { 679 after = [ 680 "go2rtc.service" 681 "network.target" 682 ]; 683 wantedBy = [ 684 "multi-user.target" 685 ]; 686 environment = { 687 CONFIG_FILE = "/run/frigate/frigate.yml"; 688 HOME = "/var/lib/frigate"; 689 PYTHONPATH = cfg.package.pythonPath; 690 } 691 // optionalAttrs (cfg.vaapiDriver != null) { 692 LIBVA_DRIVER_NAME = cfg.vaapiDriver; 693 } 694 // optionalAttrs withCoral { 695 LD_LIBRARY_PATH = makeLibraryPath (with pkgs; [ libedgetpu ]); 696 }; 697 path = 698 with pkgs; 699 [ 700 # unfree: 701 # config.boot.kernelPackages.nvidiaPackages.latest.bin 702 libva-utils 703 procps 704 radeontop 705 ] 706 ++ optionals (!stdenv.hostPlatform.isAarch64) [ 707 # not available on aarch64-linux 708 intel-gpu-tools 709 rocmPackages.rocminfo 710 ]; 711 serviceConfig = { 712 ExecStartPre = [ 713 (pkgs.writeShellScript "frigate-clear-cache" '' 714 shopt -s extglob 715 rm --recursive --force /var/cache/frigate/!(model_cache) 716 '') 717 (pkgs.writeShellScript "frigate-create-writable-config" '' 718 cp --no-preserve=mode ${configFile} /run/frigate/frigate.yml 719 '') 720 ]; 721 ExecStart = "${cfg.package.python.interpreter} -m frigate"; 722 Restart = "on-failure"; 723 SyslogIdentifier = "frigate"; 724 725 User = "frigate"; 726 Group = "frigate"; 727 SupplementaryGroups = [ "render" ] ++ optionals withCoral [ "coral" ]; 728 729 AmbientCapabilities = optionals (elem cfg.vaapiDriver [ 730 "i965" 731 "iHD" 732 ]) [ "CAP_PERFMON" ]; # for intel_gpu_top 733 734 UMask = "0027"; 735 736 StateDirectory = "frigate"; 737 StateDirectoryMode = "0750"; 738 739 # Caches 740 PrivateTmp = true; 741 CacheDirectory = [ 742 "frigate" 743 # https://github.com/blakeblackshear/frigate/discussions/18129 744 "frigate/model_cache" 745 ]; 746 CacheDirectoryMode = "0750"; 747 748 # Sockets/IPC 749 RuntimeDirectory = "frigate"; 750 }; 751 }; 752 }; 753}