diff options
Diffstat (limited to 'nixos/modules/services/video')
-rw-r--r-- | nixos/modules/services/video/epgstation/default.nix | 295 | ||||
-rw-r--r-- | nixos/modules/services/video/epgstation/streaming.json | 119 | ||||
-rw-r--r-- | nixos/modules/services/video/mirakurun.nix | 47 | ||||
-rw-r--r-- | nixos/modules/services/video/unifi-video.nix | 265 |
4 files changed, 722 insertions, 4 deletions
diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix new file mode 100644 index 00000000000..b13393c8983 --- /dev/null +++ b/nixos/modules/services/video/epgstation/default.nix @@ -0,0 +1,295 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.epgstation; + + username = config.users.users.epgstation.name; + groupname = config.users.users.epgstation.group; + + settingsFmt = pkgs.formats.json {}; + settingsTemplate = settingsFmt.generate "config.json" cfg.settings; + preStartScript = pkgs.writeScript "epgstation-prestart" '' + #!${pkgs.runtimeShell} + + PASSWORD="$(head -n1 "${cfg.basicAuth.passwordFile}")" + DB_PASSWORD="$(head -n1 "${cfg.database.passwordFile}")" + + # setup configuration + touch /etc/epgstation/config.json + chmod 640 /etc/epgstation/config.json + sed \ + -e "s,@password@,$PASSWORD,g" \ + -e "s,@dbPassword@,$DB_PASSWORD,g" \ + ${settingsTemplate} > /etc/epgstation/config.json + chown "${username}:${groupname}" /etc/epgstation/config.json + + # NOTE: Use password authentication, since mysqljs does not yet support auth_socket + if [ ! -e /var/lib/epgstation/db-created ]; then + ${pkgs.mariadb}/bin/mysql -e \ + "GRANT ALL ON \`${cfg.database.name}\`.* TO '${username}'@'localhost' IDENTIFIED by '$DB_PASSWORD';" + touch /var/lib/epgstation/db-created + fi + ''; + + streamingConfig = builtins.fromJSON (builtins.readFile ./streaming.json); + logConfig = { + appenders.stdout.type = "stdout"; + categories = { + default = { appenders = [ "stdout" ]; level = "info"; }; + system = { appenders = [ "stdout" ]; level = "info"; }; + access = { appenders = [ "stdout" ]; level = "info"; }; + stream = { appenders = [ "stdout" ]; level = "info"; }; + }; + }; + + defaultPassword = "INSECURE_GO_CHECK_CONFIGURATION_NIX\n"; +in +{ + options.services.epgstation = { + enable = mkEnableOption pkgs.epgstation.meta.description; + + usePreconfiguredStreaming = mkOption { + type = types.bool; + default = true; + description = '' + Use preconfigured default streaming options. + + Upstream defaults: + <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.sample.json"/> + ''; + }; + + port = mkOption { + type = types.port; + default = 20772; + description = '' + HTTP port for EPGStation to listen on. + ''; + }; + + socketioPort = mkOption { + type = types.port; + default = cfg.port + 1; + description = '' + Socket.io port for EPGStation to listen on. + ''; + }; + + clientSocketioPort = mkOption { + type = types.port; + default = cfg.socketioPort; + description = '' + Socket.io port that the web client is going to connect to. This may be + different from <option>socketioPort</option> if EPGStation is hidden + behind a reverse proxy. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open ports in the firewall for the EPGStation web interface. + + <warning> + <para> + Exposing EPGStation to the open internet is generally advised + against. Only use it inside a trusted local network, or consider + putting it behind a VPN if you want remote access. + </para> + </warning> + ''; + }; + + basicAuth = { + user = mkOption { + type = with types; nullOr str; + default = null; + example = "epgstation"; + description = '' + Basic auth username for EPGStation. If <literal>null</literal>, basic + auth will be disabled. + + <warning> + <para> + Basic authentication has known weaknesses, the most critical being + that it sends passwords over the network in clear text. Use this + feature to control access to EPGStation within your family and + friends, but don't rely on it for security. + </para> + </warning> + ''; + }; + + passwordFile = mkOption { + type = types.path; + default = pkgs.writeText "epgstation-password" defaultPassword; + example = "/run/keys/epgstation-password"; + description = '' + A file containing the password for <option>basicAuth.user</option>. + ''; + }; + }; + + database = { + name = mkOption { + type = types.str; + default = "epgstation"; + description = '' + Name of the MySQL database that holds EPGStation's data. + ''; + }; + + passwordFile = mkOption { + type = types.path; + default = pkgs.writeText "epgstation-db-password" defaultPassword; + example = "/run/keys/epgstation-db-password"; + description = '' + A file containing the password for the database named + <option>database.name</option>. + ''; + }; + }; + + settings = mkOption { + description = '' + Options to add to config.json. + + Documentation: + <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/> + ''; + + default = {}; + example = { + recPriority = 20; + conflictPriority = 10; + }; + + type = types.submodule { + freeformType = settingsFmt.type; + + options.readOnlyOnce = mkOption { + type = types.bool; + default = false; + description = "Don't reload configuration files at runtime."; + }; + + options.mirakurunPath = mkOption (let + sockPath = config.services.mirakurun.unixSocket; + in { + type = types.str; + default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}"; + example = "http://localhost:40772"; + description = "URL to connect to Mirakurun."; + }); + + options.encode = mkOption { + type = with types; listOf attrs; + description = "Encoding presets for recorded videos."; + default = [ + { name = "H264"; + cmd = "${pkgs.epgstation}/libexec/enc.sh main"; + suffix = ".mp4"; + default = true; } + { name = "H264-sub"; + cmd = "${pkgs.epgstation}/libexec/enc.sh sub"; + suffix = "-sub.mp4"; } + ]; + }; + }; + }; + }; + + config = mkIf cfg.enable { + environment.etc = { + "epgstation/operatorLogConfig.json".text = builtins.toJSON logConfig; + "epgstation/serviceLogConfig.json".text = builtins.toJSON logConfig; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = with cfg; [ port socketioPort ]; + }; + + users.users.epgstation = { + description = "EPGStation user"; + group = config.users.groups.epgstation.name; + isSystemUser = true; + }; + + users.groups.epgstation = {}; + + services.mirakurun.enable = mkDefault true; + + services.mysql = { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + # FIXME: enable once mysqljs supports auth_socket + # ensureUsers = [ { + # name = username; + # ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + # } ]; + }; + + services.epgstation.settings = let + defaultSettings = { + serverPort = cfg.port; + socketioPort = cfg.socketioPort; + clientSocketioPort = cfg.clientSocketioPort; + + dbType = mkDefault "mysql"; + mysql = { + user = username; + database = cfg.database.name; + socketPath = mkDefault "/run/mysqld/mysqld.sock"; + password = mkDefault "@dbPassword@"; + connectTimeout = mkDefault 1000; + connectionLimit = mkDefault 10; + }; + + basicAuth = mkIf (cfg.basicAuth.user != null) { + user = mkDefault cfg.basicAuth.user; + password = mkDefault "@password@"; + }; + + ffmpeg = mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg"; + ffprobe = mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe"; + + fileExtension = mkDefault ".m2ts"; + maxEncode = mkDefault 2; + maxStreaming = mkDefault 2; + }; + in + mkMerge [ + defaultSettings + (mkIf cfg.usePreconfiguredStreaming streamingConfig) + ]; + + systemd.tmpfiles.rules = [ + "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -" + "d '/var/lib/epgstation/recorded' - ${username} ${groupname} - -" + "d '/var/lib/epgstation/thumbnail' - ${username} ${groupname} - -" + ]; + + systemd.services.epgstation = { + description = pkgs.epgstation.meta.description; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + ] ++ optional config.services.mirakurun.enable "mirakurun.service" + ++ optional config.services.mysql.enable "mysql.service"; + + serviceConfig = { + ExecStart = "${pkgs.epgstation}/bin/epgstation start"; + ExecStartPre = "+${preStartScript}"; + User = username; + Group = groupname; + StateDirectory = "epgstation"; + LogsDirectory = "epgstation"; + ConfigurationDirectory = "epgstation"; + }; + }; + }; +} diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json new file mode 100644 index 00000000000..8eb99cf8558 --- /dev/null +++ b/nixos/modules/services/video/epgstation/streaming.json @@ -0,0 +1,119 @@ +{ + "liveHLS": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "180p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%" + } + ], + "liveMP4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + } + ], + "liveWebM": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + } + ], + "mpegTsStreaming": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "Original" + } + ], + "mpegTsViewer": { + "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS", + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end" + }, + "recordedDownloader": { + "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME", + "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end" + }, + "recordedStreaming": { + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1", + "vb": "3000k", + "ab": "192k" + }, + { + "name": "360p", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1", + "vb": "1500k", + "ab": "128k" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1", + "vb": "3000k", + "ab": "192k" + }, + { + "name": "360p", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1", + "vb": "1500k", + "ab": "128k" + } + ], + "mpegTs": [ + { + "name": "720p (H.264)", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1", + "vb": "3000k", + "ab": "192k" + }, + { + "name": "360p (H.264)", + "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1", + "vb": "1500k", + "ab": "128k" + } + ] + }, + "recordedHLS": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p(h265)", + "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%" + } + ], + "recordedViewer": { + "ios": "infuse://x-callback-url/play?url=http://ADDRESS", + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end" + } +} diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix index 675b67f6ebf..6ea73fa5c67 100644 --- a/nixos/modules/services/video/mirakurun.nix +++ b/nixos/modules/services/video/mirakurun.nix @@ -8,6 +8,18 @@ let username = config.users.users.mirakurun.name; groupname = config.users.users.mirakurun.group; settingsFmt = pkgs.formats.yaml {}; + + polkitRule = pkgs.writeTextDir "share/polkit-1/rules.d/10-mirakurun.rules" '' + polkit.addRule(function (action, subject) { + if ( + (action.id == "org.debian.pcsc-lite.access_pcsc" || + action.id == "org.debian.pcsc-lite.access_card") && + subject.user == "${username}" + ) { + return polkit.Result.YES; + } + }); + ''; in { options = { @@ -18,7 +30,8 @@ in type = with types; nullOr port; default = 40772; description = '' - Port to listen on. If null, it won't listen on any port. + Port to listen on. If <literal>null</literal>, it won't listen on + any port. ''; }; @@ -27,6 +40,32 @@ in default = false; description = '' Open ports in the firewall for Mirakurun. + + <warning> + <para> + Exposing Mirakurun to the open internet is generally advised + against. Only use it inside a trusted local network, or + consider putting it behind a VPN if you want remote access. + </para> + </warning> + ''; + }; + + unixSocket = mkOption { + type = with types; nullOr path; + default = "/var/run/mirakurun/mirakurun.sock"; + description = '' + Path to unix socket to listen on. If <literal>null</literal>, it + won't listen on any unix sockets. + ''; + }; + + allowSmartCardAccess = mkOption { + type = types.bool; + default = true; + description = '' + Install polkit rules to allow Mirakurun to access smart card readers + which is commonly used along with tuner devices. ''; }; @@ -92,7 +131,7 @@ in }; config = mkIf cfg.enable { - environment.systemPackages = [ mirakurun ]; + environment.systemPackages = [ mirakurun ] ++ optional cfg.allowSmartCardAccess polkitRule; environment.etc = { "mirakurun/server.yml".source = settingsFmt.generate "server.yml" cfg.serverSettings; "mirakurun/tuners.yml" = mkIf (cfg.tunerSettings != null) { @@ -121,8 +160,8 @@ in services.mirakurun.serverSettings = { logLevel = mkDefault 2; - path = mkDefault "/var/run/mirakurun/mirakurun.sock"; - port = mkIf (cfg.port != null) (mkDefault cfg.port); + path = mkIf (cfg.unixSocket != null) cfg.unixSocket; + port = mkIf (cfg.port != null) cfg.port; }; systemd.tmpfiles.rules = [ diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix new file mode 100644 index 00000000000..d4c0268ed66 --- /dev/null +++ b/nixos/modules/services/video/unifi-video.nix @@ -0,0 +1,265 @@ +{ config, lib, pkgs, utils, ... }: +with lib; +let + cfg = config.services.unifi-video; + mainClass = "com.ubnt.airvision.Main"; + cmd = '' + ${pkgs.jsvc}/bin/jsvc \ + -cwd ${stateDir} \ + -debug \ + -verbose:class \ + -nodetach \ + -user unifi-video \ + -home ${cfg.jrePackage}/lib/openjdk \ + -cp ${pkgs.commonsDaemon}/share/java/commons-daemon-1.2.4.jar:${stateDir}/lib/airvision.jar \ + -pidfile ${cfg.pidFile} \ + -procname unifi-video \ + -Djava.security.egd=file:/dev/./urandom \ + -Xmx${cfg.maximumJavaHeapSize}M \ + -Xss512K \ + -XX:+UseG1GC \ + -XX:+UseStringDeduplication \ + -XX:MaxMetaspaceSize=768M \ + -Djava.library.path=${stateDir}/lib \ + -Djava.awt.headless=true \ + -Djavax.net.ssl.trustStore=${stateDir}/etc/ufv-truststore \ + -Dfile.encoding=UTF-8 \ + -Dav.tempdir=/var/cache/unifi-video + ''; + + mongoConf = pkgs.writeTextFile { + name = "mongo.conf"; + executable = false; + text = '' + # for documentation of all options, see http://docs.mongodb.org/manual/reference/configuration-options/ + + storage: + dbPath: ${cfg.dataDir}/db + journal: + enabled: true + syncPeriodSecs: 60 + + systemLog: + destination: file + logAppend: true + path: ${stateDir}/logs/mongod.log + + net: + port: 7441 + bindIp: 127.0.0.1 + http: + enabled: false + + operationProfiling: + slowOpThresholdMs: 500 + mode: off + ''; + }; + + + mongoWtConf = pkgs.writeTextFile { + name = "mongowt.conf"; + executable = false; + text = '' + # for documentation of all options, see: + # http://docs.mongodb.org/manual/reference/configuration-options/ + + storage: + dbPath: ${cfg.dataDir}/db-wt + journal: + enabled: true + wiredTiger: + engineConfig: + cacheSizeGB: 1 + + systemLog: + destination: file + logAppend: true + path: logs/mongod.log + + net: + port: 7441 + bindIp: 127.0.0.1 + + operationProfiling: + slowOpThresholdMs: 500 + mode: off + ''; + }; + + stateDir = "/var/lib/unifi-video"; + +in + { + + options.services.unifi-video = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether or not to enable the unifi-video service. + ''; + }; + + jrePackage = mkOption { + type = types.package; + default = pkgs.jre8; + defaultText = "pkgs.jre8"; + description = '' + The JRE package to use. Check the release notes to ensure it is supported. + ''; + }; + + unifiVideoPackage = mkOption { + type = types.package; + default = pkgs.unifi-video; + defaultText = "pkgs.unifi-video"; + description = '' + The unifi-video package to use. + ''; + }; + + mongodbPackage = mkOption { + type = types.package; + default = pkgs.mongodb-4_0; + defaultText = "pkgs.mongodb"; + description = '' + The mongodb package to use. + ''; + }; + + logDir = mkOption { + type = types.str; + default = "${stateDir}/logs"; + description = '' + Where to store the logs. + ''; + }; + + dataDir = mkOption { + type = types.str; + default = "${stateDir}/data"; + description = '' + Where to store the database and other data. + ''; + }; + + openPorts = mkOption { + type = types.bool; + default = true; + description = '' + Whether or not to open the required ports on the firewall. + ''; + }; + + maximumJavaHeapSize = mkOption { + type = types.nullOr types.int; + default = 1024; + example = 4096; + description = '' + Set the maximimum heap size for the JVM in MB. + ''; + }; + + pidFile = mkOption { + type = types.path; + default = "${cfg.dataDir}/unifi-video.pid"; + description = "Location of unifi-video pid file."; + }; + +}; + +config = mkIf cfg.enable { + users = { + users.unifi-video = { + description = "UniFi Video controller daemon user"; + home = stateDir; + group = "unifi-video"; + isSystemUser = true; + }; + groups.unifi-video = {}; + }; + + networking.firewall = mkIf cfg.openPorts { + # https://help.ui.com/hc/en-us/articles/217875218-UniFi-Video-Ports-Used + allowedTCPPorts = [ + 7080 # HTTP portal + 7443 # HTTPS portal + 7445 # Video over HTTP (mobile app) + 7446 # Video over HTTPS (mobile app) + 7447 # RTSP via the controller + 7442 # Camera management from cameras to NVR over WAN + ]; + allowedUDPPorts = [ + 6666 # Inbound camera streams sent over WAN + ]; + }; + + systemd.tmpfiles.rules = [ + "d '${stateDir}' 0700 unifi-video unifi-video - -" + "d '/var/cache/unifi-video' 0700 unifi-video unifi-video - -" + + "d '${stateDir}/logs' 0700 unifi-video unifi-video - -" + "C '${stateDir}/etc' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc" + "C '${stateDir}/webapps' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/webapps" + "C '${stateDir}/email' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/email" + "C '${stateDir}/fw' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/fw" + "C '${stateDir}/lib' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/lib" + + "d '${stateDir}/data' 0700 unifi-video unifi-video - -" + "d '${stateDir}/data/db' 0700 unifi-video unifi-video - -" + "C '${stateDir}/data/system.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc/system.properties" + + "d '${stateDir}/bin' 0700 unifi-video unifi-video - -" + "f '${stateDir}/bin/evostreamms' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/evostreamms" + "f '${stateDir}/bin/libavcodec.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavcodec.so.54" + "f '${stateDir}/bin/libavformat.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavformat.so.54" + "f '${stateDir}/bin/libavutil.so.52' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavutil.so.52" + "f '${stateDir}/bin/ubnt.avtool' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.avtool" + "f '${stateDir}/bin/ubnt.updater' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.updater" + "C '${stateDir}/bin/mongo' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongo" + "C '${stateDir}/bin/mongod' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongod" + "C '${stateDir}/bin/mongoperf' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongoperf" + "C '${stateDir}/bin/mongos' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongos" + + "d '${stateDir}/conf' 0700 unifi-video unifi-video - -" + "C '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/evostream" + "Z '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - -" + "L+ '${stateDir}/conf/mongodv3.0+.conf' 0700 unifi-video unifi-video - ${mongoConf}" + "L+ '${stateDir}/conf/mongodv3.6+.conf' 0700 unifi-video unifi-video - ${mongoConf}" + "L+ '${stateDir}/conf/mongod-wt.conf' 0700 unifi-video unifi-video - ${mongoWtConf}" + "L+ '${stateDir}/conf/catalina.policy' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.policy" + "L+ '${stateDir}/conf/catalina.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.properties" + "L+ '${stateDir}/conf/context.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/context.xml" + "L+ '${stateDir}/conf/logging.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/logging.properties" + "L+ '${stateDir}/conf/server.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/server.xml" + "L+ '${stateDir}/conf/tomcat-users.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/tomcat-users.xml" + "L+ '${stateDir}/conf/web.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/web.xml" + + ]; + + systemd.services.unifi-video = { + description = "UniFi Video NVR daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] ; + unitConfig.RequiresMountsFor = stateDir; + # Make sure package upgrades trigger a service restart + restartTriggers = [ cfg.unifiVideoPackage cfg.mongodbPackage ]; + path = with pkgs; [ gawk coreutils busybox which jre8 lsb-release libcap util-linux ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${(removeSuffix "\n" cmd)} ${mainClass} start"; + ExecStop = "${(removeSuffix "\n" cmd)} stop ${mainClass} stop"; + Restart = "on-failure"; + UMask = "0077"; + User = "unifi-video"; + WorkingDirectory = "${stateDir}"; + }; + }; + + }; + + meta = { + maintainers = with lib.maintainers; [ rsynnest ]; + }; +} |