diff options
Diffstat (limited to 'nixos/modules/services/video')
-rw-r--r-- | nixos/modules/services/video/epgstation/default.nix | 334 | ||||
-rw-r--r-- | nixos/modules/services/video/epgstation/streaming.json | 140 | ||||
-rw-r--r-- | nixos/modules/services/video/mirakurun.nix | 204 | ||||
-rw-r--r-- | nixos/modules/services/video/replay-sorcery.nix | 72 | ||||
-rw-r--r-- | nixos/modules/services/video/rtsp-simple-server.nix | 80 | ||||
-rw-r--r-- | nixos/modules/services/video/unifi-video.nix | 267 |
6 files changed, 1097 insertions, 0 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..191f6eb52e5 --- /dev/null +++ b/nixos/modules/services/video/epgstation/default.nix @@ -0,0 +1,334 @@ +{ config, lib, options, pkgs, ... }: + +let + cfg = config.services.epgstation; + opt = options.services.epgstation; + + description = "EPGStation: DVR system for Mirakurun-managed TV tuners"; + + username = config.users.users.epgstation.name; + groupname = config.users.users.epgstation.group; + mirakurun = { + sock = config.services.mirakurun.unixSocket; + option = options.services.mirakurun.unixSocket; + }; + + yaml = pkgs.formats.yaml { }; + settingsTemplate = yaml.generate "config.yml" cfg.settings; + preStartScript = pkgs.writeScript "epgstation-prestart" '' + #!${pkgs.runtimeShell} + + DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile} + + if [[ ! -f "$DB_PASSWORD_FILE" ]]; then + printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \ + "$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2 + exit 1 + fi + + DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})" + + # setup configuration + touch /etc/epgstation/config.yml + chmod 640 /etc/epgstation/config.yml + sed \ + -e "s,@dbPassword@,$DB_PASSWORD,g" \ + ${settingsTemplate} > /etc/epgstation/config.yml + chown "${username}:${groupname}" /etc/epgstation/config.yml + + # 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 = lib.importJSON ./streaming.json; + logConfig = yaml.generate "logConfig.yml" { + 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"; }; + }; + }; + + # Deprecate top level options that are redundant. + deprecateTopLevelOption = config: + lib.mkRenamedOptionModule + ([ "services" "epgstation" ] ++ config) + ([ "services" "epgstation" "settings" ] ++ config); + + removeOption = config: instruction: + lib.mkRemovedOptionModule + ([ "services" "epgstation" ] ++ config) + instruction; +in +{ + meta.maintainers = with lib.maintainers; [ midchildan ]; + + imports = [ + (deprecateTopLevelOption [ "port" ]) + (deprecateTopLevelOption [ "socketioPort" ]) + (deprecateTopLevelOption [ "clientSocketioPort" ]) + (removeOption [ "basicAuth" ] + "Use a TLS-terminated reverse proxy with authentication instead.") + ]; + + options.services.epgstation = { + enable = lib.mkEnableOption description; + + package = lib.mkOption { + default = pkgs.epgstation; + type = lib.types.package; + defaultText = lib.literalExpression "pkgs.epgstation"; + description = "epgstation package to use"; + }; + + usePreconfiguredStreaming = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Use preconfigured default streaming options. + + Upstream defaults: + <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template"/> + ''; + }; + + openFirewall = lib.mkOption { + type = lib.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> + ''; + }; + + database = { + name = lib.mkOption { + type = lib.types.str; + default = "epgstation"; + description = '' + Name of the MySQL database that holds EPGStation's data. + ''; + }; + + passwordFile = lib.mkOption { + type = lib.types.path; + example = "/run/keys/epgstation-db-password"; + description = '' + A file containing the password for the database named + <option>database.name</option>. + ''; + }; + }; + + # The defaults for some options come from the upstream template + # configuration, which is the one that users would get if they follow the + # upstream instructions. This is, in some cases, different from the + # application defaults. Some options like encodeProcessNum and + # concurrentEncodeNum doesn't have an optimal default value that works for + # all hardware setups and/or performance requirements. For those kind of + # options, the application default wouldn't always result in the expected + # out-of-the-box behavior because it's the responsibility of the user to + # configure them according to their needs. In these cases, the value in the + # upstream template configuration should serve as a "good enough" default. + settings = lib.mkOption { + description = '' + Options to add to config.yml. + + Documentation: + <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/> + ''; + + default = { }; + example = { + recPriority = 20; + conflictPriority = 10; + }; + + type = lib.types.submodule { + freeformType = yaml.type; + + options.port = lib.mkOption { + type = lib.types.port; + default = 20772; + description = '' + HTTP port for EPGStation to listen on. + ''; + }; + + options.socketioPort = lib.mkOption { + type = lib.types.port; + default = cfg.settings.port + 1; + defaultText = lib.literalExpression "config.${opt.settings}.port + 1"; + description = '' + Socket.io port for EPGStation to listen on. It is valid to share + ports with <option>${opt.settings}.port</option>. + ''; + }; + + options.clientSocketioPort = lib.mkOption { + type = lib.types.port; + default = cfg.settings.socketioPort; + defaultText = lib.literalExpression "config.${opt.settings}.socketioPort"; + description = '' + Socket.io port that the web client is going to connect to. This may + be different from <option>${opt.settings}.socketioPort</option> if + EPGStation is hidden behind a reverse proxy. + ''; + }; + + options.mirakurunPath = with mirakurun; lib.mkOption { + type = lib.types.str; + default = "http+unix://${lib.replaceStrings ["/"] ["%2F"] sock}"; + defaultText = lib.literalExpression '' + "http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}" + ''; + example = "http://localhost:40772"; + description = "URL to connect to Mirakurun."; + }; + + options.encodeProcessNum = lib.mkOption { + type = lib.types.ints.positive; + default = 4; + description = '' + The maximum number of processes that EPGStation would allow to run + at the same time for encoding or streaming videos. + ''; + }; + + options.concurrentEncodeNum = lib.mkOption { + type = lib.types.ints.positive; + default = 1; + description = '' + The maximum number of encoding jobs that EPGStation would run at the + same time. + ''; + }; + + options.encode = lib.mkOption { + type = with lib.types; listOf attrs; + description = "Encoding presets for recorded videos."; + default = [ + { + name = "H.264"; + cmd = "%NODE% ${cfg.package}/libexec/enc.js"; + suffix = ".mp4"; + } + ]; + defaultText = lib.literalExpression '' + [ + { + name = "H.264"; + cmd = "%NODE% config.${opt.package}/libexec/enc.js"; + suffix = ".mp4"; + } + ] + ''; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings); + message = '' + The option config.${opt.settings}.readOnlyOnce can no longer be used + since it's been removed. No replacements are available. + ''; + } + ]; + + environment.etc = { + "epgstation/epgUpdaterLogConfig.yml".source = logConfig; + "epgstation/operatorLogConfig.yml".source = logConfig; + "epgstation/serviceLogConfig.yml".source = logConfig; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = with cfg.settings; [ port socketioPort ]; + }; + + users.users.epgstation = { + description = "EPGStation user"; + group = config.users.groups.epgstation.name; + isSystemUser = true; + }; + + users.groups.epgstation = { }; + + services.mirakurun.enable = lib.mkDefault true; + + services.mysql = { + enable = lib.mkDefault true; + package = lib.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 = { + dbtype = lib.mkDefault "mysql"; + mysql = { + socketPath = lib.mkDefault "/run/mysqld/mysqld.sock"; + user = username; + password = lib.mkDefault "@dbPassword@"; + database = cfg.database.name; + }; + + ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg"; + ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe"; + + # for disambiguation with TypeScript files + recordedFileExtension = lib.mkDefault ".m2ts"; + }; + in + lib.mkMerge [ + defaultSettings + (lib.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 = { + inherit description; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] + ++ lib.optional config.services.mirakurun.enable "mirakurun.service" + ++ lib.optional config.services.mysql.enable "mysql.service"; + + serviceConfig = { + ExecStart = "${cfg.package}/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..7f8df0817fc --- /dev/null +++ b/nixos/modules/services/video/epgstation/streaming.json @@ -0,0 +1,140 @@ +{ + "urlscheme": { + "m2ts": { + "ios": "vlc-x-callback://x-callback-url/stream?url=PROTOCOL://ADDRESS", + "android": "intent://ADDRESS#Intent;package=org.videolan.vlc;type=video;scheme=PROTOCOL;end" + }, + "video": { + "ios": "infuse://x-callback-url/play?url=PROTOCOL://ADDRESS", + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=PROTOCOL;end" + }, + "download": { + "ios": "vlc-x-callback://x-callback-url/download?url=PROTOCOL://ADDRESS&filename=FILENAME" + } + }, + "stream": { + "live": { + "ts": { + "m2ts": [ + { + "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": "無変換" + } + ], + "m2tsll": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1" + } + ], + "webm": [ + { + "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" + } + ], + "mp4": [ + { + "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" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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 -map 0 -threads 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 -hls_flags delete_segments -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%" + } + ] + } + }, + "recorded": { + "ts": { + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -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% -dual_mono_mode main -i pipe:0 -sn -threads 3 -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" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -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% -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" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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 pipe:0 -sn -map 0 -threads 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 -hls_flags delete_segments -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%" + } + ] + }, + "encoded": { + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf 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% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf 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" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 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 -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 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 -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + } + ] + } + } + } +} diff --git a/nixos/modules/services/video/mirakurun.nix b/nixos/modules/services/video/mirakurun.nix new file mode 100644 index 00000000000..35303b2332c --- /dev/null +++ b/nixos/modules/services/video/mirakurun.nix @@ -0,0 +1,204 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mirakurun; + mirakurun = pkgs.mirakurun; + 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 = { + services.mirakurun = { + enable = mkEnableOption "the Mirakurun DVR Tuner Server"; + + port = mkOption { + type = with types; nullOr port; + default = 40772; + description = '' + Port to listen on. If <literal>null</literal>, it won't listen on + any port. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + 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. + ''; + }; + + serverSettings = mkOption { + type = settingsFmt.type; + default = {}; + example = literalExpression '' + { + highWaterMark = 25165824; + overflowTimeLimit = 30000; + }; + ''; + description = '' + Options for server.yml. + + Documentation: + <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/> + ''; + }; + + tunerSettings = mkOption { + type = with types; nullOr settingsFmt.type; + default = null; + example = literalExpression '' + [ + { + name = "tuner-name"; + types = [ "GR" "BS" "CS" "SKY" ]; + dvbDevicePath = "/dev/dvb/adapterX/dvrX"; + } + ]; + ''; + description = '' + Options which are added to tuners.yml. If none is specified, it will + automatically be generated at runtime. + + Documentation: + <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/> + ''; + }; + + channelSettings = mkOption { + type = with types; nullOr settingsFmt.type; + default = null; + example = literalExpression '' + [ + { + name = "channel"; + types = "GR"; + channel = "0"; + } + ]; + ''; + description = '' + Options which are added to channels.yml. If none is specified, it + will automatically be generated at runtime. + + Documentation: + <link xlink:href="https://github.com/Chinachu/Mirakurun/blob/master/doc/Configuration.md"/> + ''; + }; + }; + }; + + config = mkIf cfg.enable { + 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) { + source = settingsFmt.generate "tuners.yml" cfg.tunerSettings; + mode = "0644"; + user = username; + group = groupname; + }; + "mirakurun/channels.yml" = mkIf (cfg.channelSettings != null) { + source = settingsFmt.generate "channels.yml" cfg.channelSettings; + mode = "0644"; + user = username; + group = groupname; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = mkIf (cfg.port != null) [ cfg.port ]; + }; + + users.users.mirakurun = { + description = "Mirakurun user"; + group = "video"; + isSystemUser = true; + }; + + services.mirakurun.serverSettings = { + logLevel = mkDefault 2; + path = mkIf (cfg.unixSocket != null) cfg.unixSocket; + port = mkIf (cfg.port != null) cfg.port; + }; + + systemd.tmpfiles.rules = [ + "d '/etc/mirakurun' - ${username} ${groupname} - -" + ]; + + systemd.services.mirakurun = { + description = mirakurun.meta.description; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${mirakurun}/bin/mirakurun-start"; + User = username; + Group = groupname; + RuntimeDirectory="mirakurun"; + StateDirectory="mirakurun"; + Nice = -10; + IOSchedulingClass = "realtime"; + IOSchedulingPriority = 7; + }; + + environment = { + SERVER_CONFIG_PATH = "/etc/mirakurun/server.yml"; + TUNERS_CONFIG_PATH = "/etc/mirakurun/tuners.yml"; + CHANNELS_CONFIG_PATH = "/etc/mirakurun/channels.yml"; + SERVICES_DB_PATH = "/var/lib/mirakurun/services.json"; + PROGRAMS_DB_PATH = "/var/lib/mirakurun/programs.json"; + NODE_ENV = "production"; + }; + + restartTriggers = let + getconf = target: config.environment.etc."mirakurun/${target}.yml".source; + targets = [ + "server" + ] ++ optional (cfg.tunerSettings != null) "tuners" + ++ optional (cfg.channelSettings != null) "channels"; + in (map getconf targets); + }; + }; + } diff --git a/nixos/modules/services/video/replay-sorcery.nix b/nixos/modules/services/video/replay-sorcery.nix new file mode 100644 index 00000000000..abe7202a4a8 --- /dev/null +++ b/nixos/modules/services/video/replay-sorcery.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.replay-sorcery; + configFile = generators.toKeyValue {} cfg.settings; +in +{ + options = with types; { + services.replay-sorcery = { + enable = mkEnableOption "the ReplaySorcery service for instant-replays"; + + enableSysAdminCapability = mkEnableOption '' + the system admin capability to support hardware accelerated + video capture. This is equivalent to running ReplaySorcery as + root, so use with caution''; + + autoStart = mkOption { + type = bool; + default = false; + description = "Automatically start ReplaySorcery when graphical-session.target starts."; + }; + + settings = mkOption { + type = attrsOf (oneOf [ str int ]); + default = {}; + description = "System-wide configuration for ReplaySorcery (/etc/replay-sorcery.conf)."; + example = literalExpression '' + { + videoInput = "hwaccel"; # requires `services.replay-sorcery.enableSysAdminCapability = true` + videoFramerate = 60; + } + ''; + }; + }; + }; + + config = mkIf cfg.enable { + environment = { + systemPackages = [ pkgs.replay-sorcery ]; + etc."replay-sorcery.conf".text = configFile; + }; + + security.wrappers = mkIf cfg.enableSysAdminCapability { + replay-sorcery = { + owner = "root"; + group = "root"; + capabilities = "cap_sys_admin+ep"; + source = "${pkgs.replay-sorcery}/bin/replay-sorcery"; + }; + }; + + systemd = { + packages = [ pkgs.replay-sorcery ]; + user.services.replay-sorcery = { + wantedBy = mkIf cfg.autoStart [ "graphical-session.target" ]; + partOf = mkIf cfg.autoStart [ "graphical-session.target" ]; + serviceConfig = { + ExecStart = mkIf cfg.enableSysAdminCapability [ + "" # Tell systemd to clear the existing ExecStart list, to prevent appending to it. + "${config.security.wrapperDir}/replay-sorcery" + ]; + }; + }; + }; + }; + + meta = { + maintainers = with maintainers; [ kira-bruneau ]; + }; +} diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/rtsp-simple-server.nix new file mode 100644 index 00000000000..644b1945a1e --- /dev/null +++ b/nixos/modules/services/video/rtsp-simple-server.nix @@ -0,0 +1,80 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.rtsp-simple-server; + package = pkgs.rtsp-simple-server; + format = pkgs.formats.yaml {}; +in +{ + options = { + services.rtsp-simple-server = { + enable = mkEnableOption "RTSP Simple Server"; + + settings = mkOption { + description = '' + Settings for rtsp-simple-server. + Read more at <link xlink:href="https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml"/> + ''; + type = format.type; + + default = { + logLevel = "info"; + logDestinations = [ + "stdout" + ]; + # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default. + logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log"; + }; + + example = { + paths = { + cam = { + runOnInit = "ffmpeg -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH"; + runOnInitRestart = true; + }; + }; + }; + }; + + env = mkOption { + type = with types; attrsOf anything; + description = "Extra environment variables for RTSP Simple Server"; + default = {}; + example = { + RTSP_CONFKEY = "mykey"; + }; + }; + }; + }; + + config = mkIf (cfg.enable) { + # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes + environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings; + + systemd.services.rtsp-simple-server = { + environment = cfg.env; + + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + path = with pkgs; [ + ffmpeg + ]; + + serviceConfig = { + DynamicUser = true; + User = "rtsp-simple-server"; + Group = "rtsp-simple-server"; + + LogsDirectory = "rtsp-simple-server"; + + # user likely may want to stream cameras, can't hurt to add video group + SupplementaryGroups = "video"; + + ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml"; + }; + }; + }; +} diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix new file mode 100644 index 00000000000..43208a9fe4c --- /dev/null +++ b/nixos/modules/services/video/unifi-video.nix @@ -0,0 +1,267 @@ +{ config, lib, options, pkgs, utils, ... }: +with lib; +let + cfg = config.services.unifi-video; + opt = options.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 = literalExpression "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 = literalExpression "pkgs.unifi-video"; + description = '' + The unifi-video package to use. + ''; + }; + + mongodbPackage = mkOption { + type = types.package; + default = pkgs.mongodb-4_0; + defaultText = literalExpression "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"; + defaultText = literalExpression ''"''${config.${opt.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 ]; + }; +} |