diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 3bb993ec33c6fec..2f2f6af523525ea 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -201,6 +201,8 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m - [watchdogd](https://troglobit.com/projects/watchdogd/), a system and process supervisor using watchdog timers. Available as [services.watchdogd](#opt-services.watchdogd.enable). +- [WiVRn](https://github.com/Meumeu/WiVRn), an OpenXR streaming application. Available as [services.wivrn](#opt-services.wivrn.enable). + - [Workout-tracker](https://github.com/jovandeginste/workout-tracker), a workout tracking web application for personal use. - [wyoming-satellite](https://github.com/rhasspy/wyoming-satellite), a voice assistant satellite for Home Assistant using the Wyoming protocol. Available as [services.wyoming.satellite](#opt-services.wyoming.satellite.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 210ca98e2f275b4..3cab930046af8f9 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1367,6 +1367,7 @@ ./services/video/mediamtx.nix ./services/video/unifi-video.nix ./services/video/v4l2-relayd.nix + ./services/video/wivrn.nix ./services/wayland/cage.nix ./services/wayland/hypridle.nix ./services/web-apps/akkoma.nix diff --git a/nixos/modules/services/video/wivrn.nix b/nixos/modules/services/video/wivrn.nix new file mode 100644 index 000000000000000..f571832053ad6e9 --- /dev/null +++ b/nixos/modules/services/video/wivrn.nix @@ -0,0 +1,226 @@ +{ + config, + pkgs, + lib, + ... +}: +let + inherit (lib) + mkIf + mkEnableOption + mkPackageOption + mkOption + optionalString + optionalAttrs + isDerivation + recursiveUpdate + getExe + literalExpression + types + maintainers + ; + cfg = config.services.wivrn; + configFormat = pkgs.formats.json { }; + + # For the application option to work with systemd PATH, we find the store binary path of + # the package, concat all of the following strings, and then update the application attribute. + # Application can either be a package or a list that has a package as the first element. + applicationExists = builtins.hasAttr "application" cfg.config.json; + applicationListNotEmpty = ( + if builtins.isList cfg.config.json.application then + (builtins.length cfg.config.json.application) != 0 + else + true + ); + applicationCheck = applicationExists && applicationListNotEmpty; + + applicationBinary = ( + if builtins.isList cfg.config.json.application then + builtins.head cfg.config.json.application + else + cfg.config.json.application + ); + applicationStrings = builtins.tail cfg.config.json.application; + + applicationPath = mkIf applicationCheck applicationBinary; + + applicationConcat = ( + if builtins.isList cfg.config.json.application then + builtins.concatStringsSep " " ([ (getExe applicationBinary) ] ++ applicationStrings) + else + (getExe applicationBinary) + ); + applicationUpdate = recursiveUpdate cfg.config.json ( + optionalAttrs applicationCheck { application = applicationConcat; } + ); + configFile = configFormat.generate "config.json" applicationUpdate; +in +{ + options = { + services.wivrn = { + enable = mkEnableOption "WiVRn, an OpenXR streaming application"; + + package = mkPackageOption pkgs "wivrn" { }; + + openFirewall = mkEnableOption "the default ports in the firewall for the WiVRn server"; + + defaultRuntime = mkEnableOption '' + WiVRn Monado as the default OpenXR runtime on the system. + The config can be found at `/etc/xdg/openxr/1/active_runtime.json`. + + Note that applications can bypass this option by setting an active + runtime in a writable XDG_CONFIG_DIRS location like `~/.config` + ''; + + autoStart = mkEnableOption "starting the service by default"; + + monadoEnvironment = mkOption { + type = types.attrs; + description = "Environment variables to be passed to the Monado environment."; + default = { + XRT_COMPOSITOR_LOG = "debug"; + XRT_PRINT_OPTIONS = "on"; + IPC_EXIT_ON_DISCONNECT = "off"; + }; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + description = "Packages to add to the wivrn-application service $PATH."; + default = [ ]; + example = literalExpression "[ pkgs.bash pkgs.procps ]"; + }; + + config = { + enable = mkEnableOption "configuration for WiVRn"; + json = mkOption { + type = configFormat.type; + description = '' + Configuration for WiVRn. The attributes are serialized to JSON in config.json. + + Note that the application option must be either a package or a + list with package as the first element. + + See https://github.com/Meumeu/WiVRn/blob/master/docs/configuration.md + ''; + default = { }; + example = literalExpression '' + { + scale = 0.8; + bitrate = 100000000; + encoders = [ + { + encoder = "nvenc"; + codec = "h264"; + width = 1.0; + height = 1.0; + offset_x = 0.0; + offset_y = 0.0; + } + ]; + application = [ pkgs.wlx-overlay-s ]; + tcp_only = true; + } + ''; + }; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !applicationCheck || isDerivation applicationBinary; + message = "The application in WiVRn configuration is not a package. Please ensure that the application is a package or that a package is the first element in the list."; + } + ]; + + systemd.user = { + services = { + # The WiVRn server runs in a hardened service and starts the applications in a different service + wivrn = { + description = "WiVRn XR runtime service"; + environment = { + # Default options + # https://gitlab.freedesktop.org/monado/monado/-/blob/598080453545c6bf313829e5780ffb7dde9b79dc/src/xrt/targets/service/monado.in.service#L12 + XRT_COMPOSITOR_LOG = "debug"; + XRT_PRINT_OPTIONS = "on"; + IPC_EXIT_ON_DISCONNECT = "off"; + } // cfg.monadoEnvironment; + serviceConfig = { + ExecStart = ( + (getExe cfg.package) + " --systemd" + optionalString cfg.config.enable " -f ${configFile}" + ); + # Hardening options + CapabilityBoundingSet = [ "CAP_SYS_NICE" ]; + AmbientCapabilities = [ "CAP_SYS_NICE" ]; + LockPersonality = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + }; + wantedBy = mkIf cfg.autoStart [ "default.target" ]; + restartTriggers = [ + cfg.package + configFile + ]; + }; + wivrn-application = mkIf applicationCheck { + description = "WiVRn application service"; + requires = [ "wivrn.service" ]; + serviceConfig = { + ExecStart = ( + (getExe cfg.package) + " --application" + optionalString cfg.config.enable " -f ${configFile}" + ); + Restart = "on-failure"; + RestartSec = 0; + PrivateTmp = true; + }; + # We need to add the application to PATH so WiVRn can find it + path = [ applicationPath ] ++ cfg.extraPackages; + }; + }; + }; + + services = { + # WiVRn can be used with some wired headsets so we include xr-hardware + udev.packages = with pkgs; [ + android-udev-rules + xr-hardware + ]; + avahi = { + enable = true; + publish = { + enable = true; + userServices = true; + }; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ 9757 ]; + allowedUDPPorts = [ 9757 ]; + }; + + environment = { + systemPackages = [ + cfg.package + applicationPath + ]; + pathsToLink = [ "/share/openxr" ]; + etc."xdg/openxr/1/active_runtime.json" = mkIf cfg.defaultRuntime { + source = "${cfg.package}/share/openxr/1/openxr_wivrn.json"; + }; + }; + }; + meta.maintainers = with maintainers; [ passivelemon ]; +}