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() {