1{ config
2, lib
3, pkgs
4, ...
5}:
6
7let
8 inherit (lib)
9 literalExpression
10 mkDefault
11 mdDoc
12 mkEnableOption
13 mkIf
14 mkOption
15 types;
16
17 cfg = config.services.frigate;
18
19 format = pkgs.formats.yaml {};
20
21 filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! lib.elem v [ null ])) cfg.settings;
22
23 cameraFormat = with types; submodule {
24 freeformType = format.type;
25 options = {
26 ffmpeg = {
27 inputs = mkOption {
28 description = mdDoc ''
29 List of inputs for this camera.
30 '';
31 type = listOf (submodule {
32 freeformType = format.type;
33 options = {
34 path = mkOption {
35 type = str;
36 example = "rtsp://192.0.2.1:554/rtsp";
37 description = mdDoc ''
38 Stream URL
39 '';
40 };
41 roles = mkOption {
42 type = listOf (enum [ "detect" "record" "rtmp" ]);
43 example = literalExpression ''
44 [ "detect" "rtmp" ]
45 '';
46 description = mdDoc ''
47 List of roles for this stream
48 '';
49 };
50 };
51 });
52 };
53 };
54 };
55 };
56
57in
58
59{
60 meta.buildDocsInSandbox = false;
61
62 options.services.frigate = with types; {
63 enable = mkEnableOption (mdDoc "Frigate NVR");
64
65 package = mkOption {
66 type = package;
67 default = pkgs.frigate;
68 description = mdDoc ''
69 The frigate package to use.
70 '';
71 };
72
73 hostname = mkOption {
74 type = str;
75 example = "frigate.exampe.com";
76 description = mdDoc ''
77 Hostname of the nginx vhost to configure.
78
79 Only nginx is supported by upstream for direct reverse proxying.
80 '';
81 };
82
83 settings = mkOption {
84 type = submodule {
85 freeformType = format.type;
86 options = {
87 cameras = mkOption {
88 type = attrsOf cameraFormat;
89 description = mdDoc ''
90 Attribute set of cameras configurations.
91
92 https://docs.frigate.video/configuration/cameras
93 '';
94 };
95
96 database = {
97 path = mkOption {
98 type = path;
99 default = "/var/lib/frigate/frigate.db";
100 description = mdDoc ''
101 Path to the SQLite database used
102 '';
103 };
104 };
105
106 mqtt = {
107 enabled = mkEnableOption (mdDoc "MQTT support");
108
109 host = mkOption {
110 type = nullOr str;
111 default = null;
112 example = "mqtt.example.com";
113 description = mdDoc ''
114 MQTT server hostname
115 '';
116 };
117 };
118 };
119 };
120 default = {};
121 description = mdDoc ''
122 Frigate configuration as a nix attribute set.
123
124 See the project documentation for how to configure frigate.
125 - [Creating a config file](https://docs.frigate.video/guides/getting_started)
126 - [Configuration reference](https://docs.frigate.video/configuration/index)
127 '';
128 };
129 };
130
131 config = mkIf cfg.enable {
132 services.nginx = {
133 enable =true;
134 additionalModules = with pkgs.nginxModules; [
135 secure-token
136 rtmp
137 vod
138 ];
139 recommendedProxySettings = mkDefault true;
140 recommendedGzipSettings = mkDefault true;
141 upstreams = {
142 frigate-api.servers = {
143 "127.0.0.1:5001" = {};
144 };
145 frigate-mqtt-ws.servers = {
146 "127.0.0.1:5002" = {};
147 };
148 frigate-jsmpeg.servers = {
149 "127.0.0.1:8082" = {};
150 };
151 frigate-go2rtc.servers = {
152 "127.0.0.1:1984" = {};
153 };
154 };
155 # Based on https://github.com/blakeblackshear/frigate/blob/v0.12.0/docker/rootfs/usr/local/nginx/conf/nginx.conf
156 virtualHosts."${cfg.hostname}" = {
157 locations = {
158 "/api/" = {
159 proxyPass = "http://frigate-api/";
160 };
161 "~* /api/.*\.(jpg|jpeg|png)$" = {
162 proxyPass = "http://frigate-api";
163 extraConfig = ''
164 add_header 'Access-Control-Allow-Origin' '*';
165 add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
166 rewrite ^/api/(.*)$ $1 break;
167 '';
168 };
169 "/vod/" = {
170 extraConfig = ''
171 aio threads;
172 vod hls;
173
174 secure_token $args;
175 secure_token_types application/vnd.apple.mpegurl;
176
177 add_header Access-Control-Allow-Headers '*';
178 add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
179 add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
180 add_header Access-Control-Allow-Origin '*';
181 add_header Cache-Control "no-store";
182 expires off;
183 '';
184 };
185 "/stream/" = {
186 # TODO
187 };
188 "/ws" = {
189 proxyPass = "http://frigate-mqtt-ws/";
190 proxyWebsockets = true;
191 };
192 "/live/jsmpeg" = {
193 proxyPass = "http://frigate-jsmpeg/";
194 proxyWebsockets = true;
195 };
196 "/live/mse/" = {
197 proxyPass = "http://frigate-go2rtc/";
198 proxyWebsockets = true;
199 };
200 "/live/webrtc/" = {
201 proxyPass = "http://frigate-go2rtc/";
202 proxyWebsockets = true;
203 };
204 "/cache/" = {
205 alias = "/var/cache/frigate/";
206 };
207 "/clips/" = {
208 root = "/var/lib/frigate";
209 extraConfig = ''
210 add_header 'Access-Control-Allow-Origin' "$http_origin" always;
211 add_header 'Access-Control-Allow-Credentials' 'true';
212 add_header 'Access-Control-Expose-Headers' 'Content-Length';
213 if ($request_method = 'OPTIONS') {
214 add_header 'Access-Control-Allow-Origin' "$http_origin";
215 add_header 'Access-Control-Max-Age' 1728000;
216 add_header 'Content-Type' 'text/plain charset=UTF-8';
217 add_header 'Content-Length' 0;
218 return 204;
219 }
220
221 types {
222 video/mp4 mp4;
223 image/jpeg jpg;
224 }
225
226 autoindex on;
227 '';
228 };
229 "/recordings/" = {
230 root = "/var/lib/frigate";
231 extraConfig = ''
232 add_header 'Access-Control-Allow-Origin' "$http_origin" always;
233 add_header 'Access-Control-Allow-Credentials' 'true';
234 add_header 'Access-Control-Expose-Headers' 'Content-Length';
235 if ($request_method = 'OPTIONS') {
236 add_header 'Access-Control-Allow-Origin' "$http_origin";
237 add_header 'Access-Control-Max-Age' 1728000;
238 add_header 'Content-Type' 'text/plain charset=UTF-8';
239 add_header 'Content-Length' 0;
240 return 204;
241 }
242
243 types {
244 video/mp4 mp4;
245 }
246
247 autoindex on;
248 autoindex_format json;
249 '';
250 };
251 "/assets/" = {
252 root = cfg.package.web;
253 extraConfig = ''
254 access_log off;
255 expires 1y;
256 add_header Cache-Control "public";
257 '';
258 };
259 "/" = {
260 root = cfg.package.web;
261 tryFiles = "$uri $uri/ /index.html";
262 extraConfig = ''
263 add_header Cache-Control "no-store";
264 expires off;
265
266 sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
267 sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
268 sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
269 sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
270 sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
271 sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/';
272 sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl';
273 sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
274 sub_filter_types text/css application/javascript;
275 sub_filter_once off;
276 '';
277 };
278 };
279 extraConfig = ''
280 # vod settings
281 vod_base_url "";
282 vod_segments_base_url "";
283 vod_mode mapped;
284 vod_max_mapping_response_size 1m;
285 vod_upstream_location /api;
286 vod_align_segments_to_key_frames on;
287 vod_manifest_segment_durations_mode accurate;
288 vod_ignore_edit_list on;
289 vod_segment_duration 10000;
290 vod_hls_mpegts_align_frames off;
291 vod_hls_mpegts_interleave_frames on;
292 # file handle caching / aio
293 open_file_cache max=1000 inactive=5m;
294 open_file_cache_valid 2m;
295 open_file_cache_min_uses 1;
296 open_file_cache_errors on;
297 aio on;
298 # https://github.com/kaltura/nginx-vod-module#vod_open_file_thread_pool
299 vod_open_file_thread_pool default;
300 # vod caches
301 vod_metadata_cache metadata_cache 512m;
302 vod_mapping_cache mapping_cache 5m 10m;
303 # gzip manifest
304 gzip_types application/vnd.apple.mpegurl;
305 '';
306 };
307 appendConfig = ''
308 rtmp {
309 server {
310 listen 1935;
311 chunk_size 4096;
312 allow publish 127.0.0.1;
313 deny publish all;
314 allow play all;
315 application live {
316 live on;
317 record off;
318 meta copy;
319 }
320 }
321 }
322 '';
323 };
324
325 systemd.services.nginx.serviceConfig.SupplementaryGroups = [
326 "frigate"
327 ];
328
329 users.users.frigate = {
330 isSystemUser = true;
331 group = "frigate";
332 };
333 users.groups.frigate = {};
334
335 systemd.services.frigate = {
336 after = [
337 "go2rtc.service"
338 "network.target"
339 ];
340 wantedBy = [
341 "multi-user.target"
342 ];
343 environment = {
344 CONFIG_FILE = format.generate "frigate.yml" filteredConfig;
345 HOME = "/var/lib/frigate";
346 PYTHONPATH = cfg.package.pythonPath;
347 };
348 path = with pkgs; [
349 # unfree:
350 # config.boot.kernelPackages.nvidiaPackages.latest.bin
351 ffmpeg_5-headless
352 libva-utils
353 procps
354 radeontop
355 ] ++ lib.optionals (!stdenv.isAarch64) [
356 # not available on aarch64-linux
357 intel-gpu-tools
358 ];
359 serviceConfig = {
360 ExecStart = "${cfg.package.python.interpreter} -m frigate";
361
362 User = "frigate";
363 Group = "frigate";
364
365 UMask = "0027";
366
367 StateDirectory = "frigate";
368 StateDirectoryMode = "0750";
369
370 # Caches
371 PrivateTmp = true;
372 CacheDirectory = "frigate";
373 CacheDirectoryMode = "0750";
374
375 BindPaths = [
376 "/migrations:${cfg.package}/share/frigate/migrations:ro"
377 ];
378 };
379 };
380 };
381}