at master 17 kB view raw
1# PipeWire service. 2{ 3 config, 4 lib, 5 pkgs, 6 ... 7}: 8 9let 10 inherit (builtins) attrNames concatMap length; 11 inherit (lib) maintainers teams; 12 inherit (lib.attrsets) 13 attrByPath 14 attrsToList 15 concatMapAttrs 16 filterAttrs 17 ; 18 inherit (lib.lists) flatten optional optionals; 19 inherit (lib.modules) mkIf mkRemovedOptionModule; 20 inherit (lib.options) 21 literalExpression 22 mkEnableOption 23 mkOption 24 mkPackageOption 25 ; 26 inherit (lib.strings) concatMapStringsSep hasPrefix optionalString; 27 inherit (lib.types) 28 attrsOf 29 bool 30 listOf 31 package 32 ; 33 34 json = pkgs.formats.json { }; 35 mapToFiles = 36 location: config: 37 concatMapAttrs (name: value: { 38 "share/pipewire/${location}.conf.d/${name}.conf" = json.generate "${name}" value; 39 }) config; 40 extraConfigPkgFromFiles = 41 locations: filesSet: 42 pkgs.runCommand "pipewire-extra-config" { } '' 43 mkdir -p ${concatMapStringsSep " " (l: "$out/share/pipewire/${l}.conf.d") locations} 44 ${concatMapStringsSep ";" ({ name, value }: "ln -s ${value} $out/${name}") (attrsToList filesSet)} 45 ''; 46 cfg = config.services.pipewire; 47 enable32BitAlsaPlugins = 48 cfg.alsa.support32Bit && pkgs.stdenv.hostPlatform.isx86_64 && pkgs.pkgsi686Linux.pipewire != null; 49 50 # The package doesn't output to $out/lib/pipewire directly so that the 51 # overlays can use the outputs to replace the originals in FHS environments. 52 # 53 # This doesn't work in general because of missing development information. 54 jack-libs = pkgs.runCommand "jack-libs" { } '' 55 mkdir -p "$out/lib" 56 ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire" 57 ''; 58 59 configPackages = cfg.configPackages; 60 61 extraConfigPkg = extraConfigPkgFromFiles [ "pipewire" "client" "jack" "pipewire-pulse" ] ( 62 mapToFiles "pipewire" cfg.extraConfig.pipewire 63 // mapToFiles "client" cfg.extraConfig.client 64 // mapToFiles "jack" cfg.extraConfig.jack 65 // mapToFiles "pipewire-pulse" cfg.extraConfig.pipewire-pulse 66 ); 67 68 configs = pkgs.buildEnv { 69 name = "pipewire-configs"; 70 paths = 71 configPackages 72 ++ [ extraConfigPkg ] 73 ++ optionals cfg.wireplumber.enable cfg.wireplumber.configPackages; 74 pathsToLink = [ "/share/pipewire" ]; 75 }; 76 77 requiredLv2Packages = flatten ( 78 concatMap (p: attrByPath [ "passthru" "requiredLv2Packages" ] [ ] p) configPackages 79 ); 80 81 lv2Plugins = pkgs.buildEnv { 82 name = "pipewire-lv2-plugins"; 83 paths = cfg.extraLv2Packages ++ requiredLv2Packages; 84 pathsToLink = [ "/lib/lv2" ]; 85 }; 86in 87{ 88 meta.maintainers = teams.freedesktop.members ++ [ maintainers.k900 ]; 89 90 ###### interface 91 options = { 92 services.pipewire = { 93 enable = mkEnableOption "PipeWire service"; 94 95 package = mkPackageOption pkgs "pipewire" { }; 96 97 socketActivation = mkOption { 98 default = true; 99 type = bool; 100 description = '' 101 Automatically run PipeWire when connections are made to the PipeWire socket. 102 ''; 103 }; 104 105 audio = { 106 enable = mkOption { 107 type = bool; 108 # this is for backwards compatibility 109 default = cfg.alsa.enable || cfg.jack.enable || cfg.pulse.enable; 110 defaultText = literalExpression "config.services.pipewire.alsa.enable || config.services.pipewire.jack.enable || config.services.pipewire.pulse.enable"; 111 description = "Whether to use PipeWire as the primary sound server"; 112 }; 113 }; 114 115 alsa = { 116 enable = mkEnableOption "ALSA support"; 117 support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems"; 118 }; 119 120 jack = { 121 enable = mkEnableOption "JACK audio emulation"; 122 }; 123 124 raopOpenFirewall = mkOption { 125 type = bool; 126 default = false; 127 description = '' 128 Opens UDP/6001-6002, required by RAOP/Airplay for timing and control data. 129 ''; 130 }; 131 132 pulse = { 133 enable = mkEnableOption "PulseAudio server emulation"; 134 }; 135 136 systemWide = mkOption { 137 type = bool; 138 default = false; 139 description = '' 140 If true, a system-wide PipeWire service and socket is enabled 141 allowing all users in the "pipewire" group to use it simultaneously. 142 If false, then user units are used instead, restricting access to 143 only one user. 144 145 Enabling system-wide PipeWire is however not recommended and disabled 146 by default according to 147 https://github.com/PipeWire/pipewire/blob/master/NEWS 148 ''; 149 }; 150 151 extraConfig = { 152 pipewire = mkOption { 153 type = attrsOf json.type; 154 default = { }; 155 example = { 156 "10-clock-rate" = { 157 "context.properties" = { 158 "default.clock.rate" = 44100; 159 }; 160 }; 161 "11-no-upmixing" = { 162 "stream.properties" = { 163 "channelmix.upmix" = false; 164 }; 165 }; 166 }; 167 description = '' 168 Additional configuration for the PipeWire server. 169 170 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/pipewire.conf.d`. 171 172 See `man pipewire.conf` for details, and [the PipeWire wiki][wiki] for examples. 173 174 See also: 175 - [PipeWire wiki - virtual devices][wiki-virtual-device] for creating virtual devices or remapping channels 176 - [PipeWire wiki - filter-chain][wiki-filter-chain] for creating more complex processing pipelines 177 - [PipeWire wiki - network][wiki-network] for streaming audio over a network 178 179 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire 180 [wiki-virtual-device]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Virtual-Devices 181 [wiki-filter-chain]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Filter-Chain 182 [wiki-network]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Network 183 ''; 184 }; 185 client = mkOption { 186 type = attrsOf json.type; 187 default = { }; 188 example = { 189 "10-no-resample" = { 190 "stream.properties" = { 191 "resample.disable" = true; 192 }; 193 }; 194 }; 195 description = '' 196 Additional configuration for the PipeWire client library, used by most applications. 197 198 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/client.conf.d`. 199 200 See the [PipeWire wiki][wiki] for examples. 201 202 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-client 203 ''; 204 }; 205 jack = mkOption { 206 type = attrsOf json.type; 207 default = { }; 208 example = { 209 "20-hide-midi" = { 210 "jack.properties" = { 211 "jack.show-midi" = false; 212 }; 213 }; 214 }; 215 description = '' 216 Additional configuration for the PipeWire JACK server and client library. 217 218 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/jack.conf.d`. 219 220 See the [PipeWire wiki][wiki] for examples. 221 222 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-JACK 223 ''; 224 }; 225 pipewire-pulse = mkOption { 226 type = attrsOf json.type; 227 default = { }; 228 example = { 229 "15-force-s16-info" = { 230 "pulse.rules" = [ 231 { 232 matches = [ 233 { "application.process.binary" = "my-broken-app"; } 234 ]; 235 actions = { 236 quirks = [ "force-s16-info" ]; 237 }; 238 } 239 ]; 240 }; 241 }; 242 description = '' 243 Additional configuration for the PipeWire PulseAudio server. 244 245 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/pipewire-pulse.conf.d`. 246 247 See `man pipewire-pulse.conf` for details, and [the PipeWire wiki][wiki] for examples. 248 249 See also: 250 - [PipeWire wiki - PulseAudio tricks guide][wiki-tricks] for more examples. 251 252 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PulseAudio 253 [wiki-tricks]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Guide-PulseAudio-Tricks 254 ''; 255 }; 256 }; 257 258 configPackages = mkOption { 259 type = listOf package; 260 default = [ ]; 261 example = literalExpression '' 262 [ 263 (pkgs.writeTextDir "share/pipewire/pipewire.conf.d/10-loopback.conf" ''' 264 context.modules = [ 265 { name = libpipewire-module-loopback 266 args = { 267 node.description = "Scarlett Focusrite Line 1" 268 capture.props = { 269 audio.position = [ FL ] 270 stream.dont-remix = true 271 node.target = "alsa_input.usb-Focusrite_Scarlett_Solo_USB_Y7ZD17C24495BC-00.analog-stereo" 272 node.passive = true 273 } 274 playback.props = { 275 node.name = "SF_mono_in_1" 276 media.class = "Audio/Source" 277 audio.position = [ MONO ] 278 } 279 } 280 } 281 ] 282 ''') 283 ]''; 284 description = '' 285 List of packages that provide PipeWire configuration, in the form of 286 `share/pipewire/*/*.conf` files. 287 288 LV2 dependencies will be picked up from config packages automatically 289 via `passthru.requiredLv2Packages`. 290 ''; 291 }; 292 293 extraLv2Packages = mkOption { 294 type = listOf package; 295 default = [ ]; 296 example = literalExpression "[ pkgs.lsp-plugins ]"; 297 description = '' 298 List of packages that provide LV2 plugins in `lib/lv2` that should 299 be made available to PipeWire for [filter chains][wiki-filter-chain]. 300 301 Config packages have their required LV2 plugins added automatically, 302 so they don't need to be specified here. Config packages need to set 303 `passthru.requiredLv2Packages` for this to work. 304 305 [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html 306 ''; 307 }; 308 }; 309 }; 310 311 imports = [ 312 (mkRemovedOptionModule [ "services" "pipewire" "config" ] '' 313 Overriding default PipeWire configuration through NixOS options never worked correctly and is no longer supported. 314 Please create drop-in configuration files via `services.pipewire.extraConfig` instead. 315 '') 316 (mkRemovedOptionModule [ "services" "pipewire" "media-session" ] '' 317 pipewire-media-session is no longer supported upstream and has been removed. 318 Please switch to `services.pipewire.wireplumber` instead. 319 '') 320 (mkRemovedOptionModule [ "services" "pipewire" "extraConfig" "client-rt" ] '' 321 `services.pipewire.extraConfig.client-rt` is no longer applicable, as `client-rt.conf` has been 322 removed upstream. Please move your customizations to `services.pipewire.extraConfig.client`. 323 '') 324 ]; 325 326 ###### implementation 327 config = mkIf cfg.enable { 328 assertions = [ 329 { 330 assertion = cfg.audio.enable -> !config.services.pulseaudio.enable; 331 message = "Using PipeWire as the sound server conflicts with PulseAudio. This option requires `services.pulseaudio.enable` to be set to false"; 332 } 333 { 334 assertion = cfg.jack.enable -> !config.services.jack.jackd.enable; 335 message = "PipeWire based JACK emulation doesn't use the JACK service. This option requires `services.jack.jackd.enable` to be set to false"; 336 } 337 { 338 # JACK intentionally not checked, as PW-on-JACK setups are a thing that some people may want 339 assertion = (cfg.alsa.enable || cfg.pulse.enable) -> cfg.audio.enable; 340 message = "Using PipeWire's ALSA/PulseAudio compatibility layers requires running PipeWire as the sound server. Either set `services.pipewire.audio.enable` to true to enable audio support, or set both `services.pipewire.pulse.enable` and `services.pipewire.alsa.enable` to false to use pipewire exclusively for the compositor."; 341 } 342 { 343 assertion = 344 length ( 345 attrNames ( 346 filterAttrs (name: value: hasPrefix "pipewire/" name || name == "pipewire") config.environment.etc 347 ) 348 ) == 1; 349 message = "Using `environment.etc.\"pipewire<...>\"` directly is no longer supported. Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` instead."; 350 } 351 ]; 352 353 environment.systemPackages = [ cfg.package ] ++ optional cfg.jack.enable jack-libs; 354 355 systemd.packages = [ cfg.package ]; 356 357 # PipeWire depends on DBUS but doesn't list it. Without this booting 358 # into a terminal results in the service crashing with an error. 359 systemd.services.pipewire.bindsTo = [ "dbus.service" ]; 360 systemd.user.services.pipewire.bindsTo = [ "dbus.service" ]; 361 362 # Enable either system or user units. 363 systemd.sockets.pipewire.enable = cfg.systemWide; 364 systemd.services.pipewire.enable = cfg.systemWide; 365 systemd.user.sockets.pipewire.enable = !cfg.systemWide; 366 systemd.user.services.pipewire.enable = !cfg.systemWide; 367 368 systemd.services.pipewire.environment.LV2_PATH = mkIf cfg.systemWide "${lv2Plugins}/lib/lv2"; 369 systemd.user.services.pipewire.environment.LV2_PATH = mkIf ( 370 !cfg.systemWide 371 ) "${lv2Plugins}/lib/lv2"; 372 systemd.user.services.filter-chain.environment.LV2_PATH = mkIf ( 373 !cfg.systemWide 374 ) "${lv2Plugins}/lib/lv2"; 375 376 # Mask pw-pulse if it's not wanted 377 systemd.services.pipewire-pulse.enable = cfg.pulse.enable && cfg.systemWide; 378 systemd.sockets.pipewire-pulse.enable = cfg.pulse.enable && cfg.systemWide; 379 systemd.user.services.pipewire-pulse.enable = cfg.pulse.enable && !cfg.systemWide; 380 systemd.user.sockets.pipewire-pulse.enable = cfg.pulse.enable && !cfg.systemWide; 381 382 systemd.sockets.pipewire.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ]; 383 systemd.sockets.pipewire-pulse.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ]; 384 systemd.user.sockets.pipewire.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ]; 385 systemd.user.sockets.pipewire-pulse.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ]; 386 387 services.udev.packages = [ cfg.package ]; 388 389 # If any paths are updated here they must also be updated in the package test. 390 environment.etc = { 391 "alsa/conf.d/49-pipewire-modules.conf" = mkIf cfg.alsa.enable { 392 text = '' 393 pcm_type.pipewire { 394 libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_pcm_pipewire.so ; 395 ${optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;"} 396 } 397 ctl_type.pipewire { 398 libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_ctl_pipewire.so ; 399 ${optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;"} 400 } 401 ''; 402 }; 403 404 "alsa/conf.d/50-pipewire.conf" = mkIf cfg.alsa.enable { 405 source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf"; 406 }; 407 408 "alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable { 409 source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf"; 410 }; 411 pipewire.source = "${configs}/share/pipewire"; 412 }; 413 414 environment.sessionVariables.LD_LIBRARY_PATH = mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ]; 415 416 networking.firewall.allowedUDPPorts = mkIf cfg.raopOpenFirewall [ 417 6001 418 6002 419 ]; 420 421 # See https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/src/modules/module-rt/25-pw-rlimits.conf.in 422 security.pam.loginLimits = [ 423 { 424 domain = "@pipewire"; 425 item = "rtprio"; 426 type = "-"; 427 value = 95; 428 } 429 { 430 domain = "@pipewire"; 431 item = "nice"; 432 type = "-"; 433 value = -19; 434 } 435 { 436 domain = "@pipewire"; 437 item = "memlock"; 438 type = "-"; 439 value = 4194304; 440 } 441 ]; 442 443 users = { 444 users.pipewire = mkIf cfg.systemWide { 445 uid = config.ids.uids.pipewire; 446 group = "pipewire"; 447 extraGroups = [ 448 "audio" 449 "video" 450 ] 451 ++ optional config.security.rtkit.enable "rtkit"; 452 description = "PipeWire system service user"; 453 isSystemUser = true; 454 home = "/var/lib/pipewire"; 455 createHome = true; 456 }; 457 groups.pipewire.gid = config.ids.gids.pipewire; 458 }; 459 }; 460}