From 36c16fa7e310af0c94aad4d5d438632057717cac Mon Sep 17 00:00:00 2001 From: midchildan Date: Mon, 10 Aug 2020 23:45:08 +0900 Subject: nixos/epgstation: add module --- nixos/modules/module-list.nix | 1 + .../modules/services/video/epgstation/default.nix | 295 +++++++++++++++++++++ nixos/modules/services/video/epgstation/generate | 31 +++ .../services/video/epgstation/streaming.json | 119 +++++++++ pkgs/applications/video/epgstation/default.nix | 1 + pkgs/applications/video/epgstation/generate.sh | 8 +- 6 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 nixos/modules/services/video/epgstation/default.nix create mode 100755 nixos/modules/services/video/epgstation/generate create mode 100644 nixos/modules/services/video/epgstation/streaming.json diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c54bc6098d3..95314358e42 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -838,6 +838,7 @@ ./services/ttys/gpm.nix ./services/ttys/kmscon.nix ./services/wayland/cage.nix + ./services/video/epgstation/default.nix ./services/video/mirakurun.nix ./services/web-apps/atlassian/confluence.nix ./services/web-apps/atlassian/crowd.nix diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix new file mode 100644 index 00000000000..8d6d431fa55 --- /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.mysql}/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: + + ''; + }; + + 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 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. + + + + 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. + + + ''; + }; + + basicAuth = { + user = mkOption { + type = with types; nullOr str; + default = null; + example = "epgstation"; + description = '' + Basic auth username for EPGStation. If null, basic + auth will be disabled. + + + + 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. + + + ''; + }; + + passwordFile = mkOption { + type = types.path; + default = pkgs.writeText "epgstation-password" defaultPassword; + example = "/run/keys/epgstation-password"; + description = '' + A file containing the password for . + ''; + }; + }; + + 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 + . + ''; + }; + }; + + settings = mkOption { + description = '' + Options to add to config.json. + + Documentation: + + ''; + + 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.mysql; + 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/generate b/nixos/modules/services/video/epgstation/generate new file mode 100755 index 00000000000..2940768b6d2 --- /dev/null +++ b/nixos/modules/services/video/epgstation/generate @@ -0,0 +1,31 @@ +#!/usr/bin/env -S nix-build --no-out-link + +# Script to generate default streaming configurations for EPGStation. There's +# no need to run this script directly since generate.sh in the EPGStation +# package directory would run this script for you. +# +# Usage: ./generate | xargs cat > streaming.json + +{ pkgs ? (import ../../../../.. {}) }: + +let + sampleConfigPath = "${pkgs.epgstation.src}/config/config.sample.json"; + sampleConfig = builtins.fromJSON (builtins.readFile sampleConfigPath); + streamingConfig = { + inherit (sampleConfig) + mpegTsStreaming + mpegTsViewer + liveHLS + liveMP4 + liveWebM + recordedDownloader + recordedStreaming + recordedViewer + recordedHLS; + }; +in +pkgs.runCommand "streaming.json" { nativeBuildInputs = [ pkgs.jq ]; } '' + jq . <<<'${builtins.toJSON streamingConfig}' > $out +'' + +# vim:set ft=nix: diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json new file mode 100644 index 00000000000..37957f6cb6a --- /dev/null +++ b/nixos/modules/services/video/epgstation/streaming.json @@ -0,0 +1,119 @@ +{ + "liveHLS": [ + { + "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": "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 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -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 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%", + "name": "180p" + } + ], + "liveMP4": [ + { + "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": "720p" + }, + { + "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", + "name": "480p" + } + ], + "liveWebM": [ + { + "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": "720p" + }, + { + "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", + "name": "480p" + } + ], + "mpegTsStreaming": [ + { + "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": "720p" + }, + { + "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": "480p" + }, + { + "name": "Original" + } + ], + "mpegTsViewer": { + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end", + "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS" + }, + "recordedDownloader": { + "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end", + "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME" + }, + "recordedHLS": [ + { + "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": "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 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%", + "name": "480p" + }, + { + "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%", + "name": "480p(h265)" + } + ], + "recordedStreaming": { + "mp4": [ + { + "ab": "192k", + "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", + "name": "720p", + "vb": "3000k" + }, + { + "ab": "128k", + "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", + "name": "360p", + "vb": "1500k" + } + ], + "mpegTs": [ + { + "ab": "192k", + "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", + "name": "720p (H.264)", + "vb": "3000k" + }, + { + "ab": "128k", + "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", + "name": "360p (H.264)", + "vb": "1500k" + } + ], + "webm": [ + { + "ab": "192k", + "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", + "name": "720p", + "vb": "3000k" + }, + { + "ab": "128k", + "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", + "name": "360p", + "vb": "1500k" + } + ] + }, + "recordedViewer": { + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end", + "ios": "infuse://x-callback-url/play?url=http://ADDRESS" + } +} diff --git a/pkgs/applications/video/epgstation/default.nix b/pkgs/applications/video/epgstation/default.nix index 6464effc560..e57f46c8c6a 100644 --- a/pkgs/applications/video/epgstation/default.nix +++ b/pkgs/applications/video/epgstation/default.nix @@ -42,6 +42,7 @@ nodePackages.epgstation.override (drv: { pushd $out/lib/node_modules/EPGStation npm run build + npm prune --production mv config/{enc.sh,enc.js} $out/libexec mv LICENSE Readme.md $out/share/doc/epgstation diff --git a/pkgs/applications/video/epgstation/generate.sh b/pkgs/applications/video/epgstation/generate.sh index 55dcf744c0c..d193a015064 100755 --- a/pkgs/applications/video/epgstation/generate.sh +++ b/pkgs/applications/video/epgstation/generate.sh @@ -17,8 +17,14 @@ main() { > package.json # regenerate node packages to update the actual Nix package - cd ../../../development/node-packages \ + pushd ../../../development/node-packages \ && ./generate.sh + popd + + # generate default streaming settings for EPGStation + pushd ../../../../nixos/modules/services/video/epgstation \ + && cat "$(./generate)" > streaming.json + popd } jq() { -- cgit 1.4.1