mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
feat(hosts): add WiVRn CLI streaming for Quest 3 #392
This commit is contained in:
1 parent
7c09a1d082
commit
3e66bbf24f
8 files changed
+426
-10
No files matched your search
@@ -382,6 +382,40 @@ game:
|
||||
del-failed:
|
||||
kubectl delete pod --all-namespaces --field-selector="status.phase==Failed"
|
||||
|
||||
# Start PC VR on idols-ai and route desktop audio to the headset.
|
||||
[linux]
|
||||
[group('vr')]
|
||||
vr:
|
||||
systemctl --user start wivrn.service
|
||||
systemctl --user stop wivrn-audio.service | complete | ignore
|
||||
systemd-run --user --collect --unit=wivrn-audio nu {{absolute_path("scripts/wivrn-audio.nu")}} switch --timeout 600
|
||||
|
||||
# Route audio to a headset that is connected right now.
|
||||
[linux]
|
||||
[group('vr')]
|
||||
vr-audio:
|
||||
nu {{absolute_path("scripts/wivrn-audio.nu")}} switch --timeout 60
|
||||
|
||||
# Stop PC VR when finished and restore the previous audio devices.
|
||||
[linux]
|
||||
[group('vr')]
|
||||
vr-stop:
|
||||
systemctl --user stop wivrn-audio.service | complete | ignore
|
||||
nu {{absolute_path("scripts/wivrn-audio.nu")}} restore
|
||||
systemctl --user stop wivrn.service
|
||||
|
||||
# Show PC VR service status.
|
||||
[linux]
|
||||
[group('vr')]
|
||||
vr-status:
|
||||
systemctl --user status wivrn.service --no-pager
|
||||
|
||||
# Follow PC VR service logs.
|
||||
[linux]
|
||||
[group('vr')]
|
||||
vr-logs:
|
||||
journalctl --user -u wivrn.service -f
|
||||
|
||||
[linux]
|
||||
[group('services')]
|
||||
list-inactive:
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Quest 3 PC VR on idols-ai
|
||||
|
||||
WiVRn streams PC VR games to Quest 3. It supplies the OpenXR runtime; xrizer translates OpenVR
|
||||
games. This host uses the server and command-line tools without the Dashboard. SteamVR does not need
|
||||
to run, but compatibility still depends on the game and translation layer.
|
||||
|
||||
## First activation and persistence
|
||||
|
||||
Run `just niri` on idols-ai from this branch's worktree, or after merging it. Log out and back in so
|
||||
Steam sees `PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1`. That variable is what makes pressure-vessel
|
||||
mount WiVRn's `$XDG_RUNTIME_DIR/wivrn/comp_ipc` socket into a game's container; without it the
|
||||
game's OpenXR client fails with `ERROR_RUNTIME_UNAVAILABLE` and the headset stays black.
|
||||
|
||||
The root filesystem is a 4 GiB tmpfs. These directories are preserved with mode 0700:
|
||||
|
||||
- `~/.config/wivrn/`: server settings, paired headset public keys and discovery cookie.
|
||||
- `~/.android/`: ADB host keys, so USB authorization survives a reboot.
|
||||
|
||||
Before the first activation, migrate any existing directories to their matching locations under
|
||||
`/persistent/home/<user>/`. Stop WiVRn first. If both locations contain data, reconcile them before
|
||||
activating; do not overwrite existing pairing state or ADB keys. Do not display or upload ADB
|
||||
private keys, or copy them into this repository.
|
||||
|
||||
OpenXR/OpenVR runtime pointers and sockets remain temporary. WiVRn switches runtime pointers while a
|
||||
headset is connected and restores previous configuration when it disconnects. Do not manage these
|
||||
pointers as read-only Home Manager files, persist IPC sockets, or delete existing runtime
|
||||
configurations blindly.
|
||||
|
||||
## Wireless connection and pairing
|
||||
|
||||
1. Connect the PC by Ethernet and Quest 3 to the trusted home LAN (`192.168.5.0/24`). Prefer 5 GHz
|
||||
or 6 GHz Wi-Fi near the access point. Guest Wi-Fi or client isolation can block discovery and
|
||||
streaming.
|
||||
2. Install the WiVRn 26.9 client on the headset: the Meta Store build is packaged as
|
||||
`org.meumeu.wivrn`, the GitHub release APK as `org.meumeu.wivrn.github`. Client and server
|
||||
versions must match; check the installed server with `wivrn-server --version` after updates.
|
||||
3. The server does not start automatically at login or boot. From this repository on idols-ai, start
|
||||
it when you want to use VR:
|
||||
|
||||
```console
|
||||
just vr
|
||||
just vr-status
|
||||
```
|
||||
|
||||
`just vr` returns immediately and leaves `wivrn-audio.service` waiting up to ten minutes for the
|
||||
headset. When it connects, that unit makes WiVRn's speaker and microphone the defaults and moves
|
||||
any stream that is already playing. `just vr-audio` does the same switch in the foreground when
|
||||
the headset is already connected, and `just vr-stop` cancels the wait and restores the saved
|
||||
devices. Starting the service does not enable it for future logins.
|
||||
|
||||
4. Allow a new headset to pair for two minutes:
|
||||
|
||||
```console
|
||||
wivrnctl pair --duration 2
|
||||
```
|
||||
|
||||
This prints a PIN. Open WiVRn on the headset, connect to `ai`, and enter the PIN. If discovery
|
||||
fails, enter `192.168.5.100` manually. Reconnection after pairing does not require running this
|
||||
command again.
|
||||
|
||||
5. Audio follows the headset: WiVRn's `wivrn.sink` and `wivrn.source` exist only while a headset is
|
||||
connected, so the switch happens then, not at `just vr` time. Check the current defaults with
|
||||
`pactl get-default-sink` and `pactl get-default-source`.
|
||||
|
||||
Manage paired headsets with `wivrnctl list-paired`, `wivrnctl rename`, or `wivrnctl unpair`. Consult
|
||||
`wivrnctl --help` and the subcommand's `--help` for arguments.
|
||||
|
||||
## Service, settings and logs
|
||||
|
||||
Manage the server through systemd so starts retain NVIDIA offload and the CAP_SYS_NICE wrapper:
|
||||
|
||||
```console
|
||||
systemctl --user restart wivrn
|
||||
just vr-stop
|
||||
just vr-status
|
||||
journalctl --user -u wivrn -b -n 100 --no-pager
|
||||
just vr-logs
|
||||
```
|
||||
|
||||
Service logs use the host's existing persistent journal. There is no separate Dashboard log folder
|
||||
or log exporter. For server settings, edit `~/.config/wivrn/config.json` following the WiVRn 26.9
|
||||
configuration schema, then restart the service. The NixOS module also supports declarative server
|
||||
configuration; an enabled declarative config takes precedence over the user file. Headset-side
|
||||
settings remain available in the headset application.
|
||||
|
||||
## Launch games on the RTX 4090
|
||||
|
||||
Set this in each VR game's Steam Properties -> Launch Options:
|
||||
|
||||
```text
|
||||
__NV_PRIME_RENDER_OFFLOAD=1 __NV_PRIME_RENDER_OFFLOAD_PROVIDER=NVIDIA-G0 __GLX_VENDOR_LIBRARY_NAME=nvidia __VK_LAYER_NV_optimus=NVIDIA_only %command%
|
||||
```
|
||||
|
||||
These are the variables the system's `nvidia-offload` wrapper sets, written out because Steam runs
|
||||
inside a bubblewrap sandbox that does not mount `/run`, so it cannot see
|
||||
`/run/current-system/sw/bin/nvidia-offload`.
|
||||
|
||||
The game has to land on the same GPU as the server. A game left on the Intel iGPU makes the server's
|
||||
swapchain allocation fail, which shows up as `vkAllocateMemory: VK_ERROR_OUT_OF_DEVICE_MEMORY` and
|
||||
`xrt_comp_create_swapchain failed` in `journalctl --user -u wivrn`, followed by an xrizer panic that
|
||||
kills the game. This applies when launching from the headset too: WiVRn starts applications in
|
||||
separate systemd units, and an already running Steam client keeps its own environment, so neither
|
||||
path inherits the server's GPU environment. For non-Steam applications, start them under
|
||||
`nvidia-offload` from a normal shell. Select a Proton version according to the game's compatibility
|
||||
reports; GE-Proton or DW-Proton alone does not guarantee VR compatibility.
|
||||
|
||||
## USB connection
|
||||
|
||||
Enable Quest developer mode, connect a data-capable USB cable, and accept the headset's USB
|
||||
debugging authorization prompt. ADB is already installed in the desktop profile. The current systemd
|
||||
handles USB access for the active local session. Developer mode is needed for USB or sideloading,
|
||||
not for the Meta Store wireless workflow.
|
||||
|
||||
For sideloading, download the matching official APK and use `adb install -r <apk-path>`. The GitHub
|
||||
release APK is packaged as `org.meumeu.wivrn.github`, the Meta Store build as `org.meumeu.wivrn`;
|
||||
pass the package you actually installed.
|
||||
|
||||
Pair over Wi-Fi first. For an already paired headset with the GitHub APK installed:
|
||||
|
||||
```console
|
||||
adb devices
|
||||
adb reverse tcp:9757 tcp:9757
|
||||
adb shell am start -a android.intent.action.VIEW -d "wivrn+tcp://127.0.0.1:9757" org.meumeu.wivrn.github
|
||||
```
|
||||
|
||||
If multiple devices are connected, add `-s <serial>` to each ADB command. Disable headset Wi-Fi
|
||||
while validating USB so the test cannot silently use wireless streaming. Recreate the reverse
|
||||
mapping after disconnecting USB.
|
||||
|
||||
## Encoding and hardware validation
|
||||
|
||||
The package enables Vulkan Video encoding but does not enable NVENC. WiVRn 26.9 probes Vulkan
|
||||
encoding before falling back to VAAPI or x264 when NVENC is absent. Check the journal's encoder
|
||||
configuration after connecting: offload variables alone do not prove hardware encoding. Vulkan
|
||||
encoding in this release supports H.264/H.265, not AV1. NVENC/AV1 requires a CUDA-enabled WiVRn
|
||||
build.
|
||||
|
||||
- Pair, connect, launch a game, and check both compositor and game GPU usage:
|
||||
`nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv` has to list both
|
||||
`wivrn-server` and the game.
|
||||
- Confirm the journal has no `vkAllocateMemory` or `xrt_comp_create_swapchain` failures, and that
|
||||
`~/.local/state/xrizer/xrizer.txt` has no `ERROR_RUNTIME_UNAVAILABLE`.
|
||||
- Restart with systemd and verify the server remains owned by `wivrn.service`.
|
||||
- Reboot the PC and reconnect without pairing again; verify saved settings and USB authorization
|
||||
remain intact.
|
||||
- Test audio routing (`just vr` and `just vr-stop`), microphone, controllers, and a supported
|
||||
OpenXR/OpenVR game.
|
||||
|
||||
Configuration and package checks do not replace these headset tests.
|
||||
@@ -0,0 +1,39 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
# WiVRn supplies the OpenXR runtime; xrizer translates OpenVR games.
|
||||
# The server uses the host GPU environment from hardware-nvidia.nix.
|
||||
services.wivrn = {
|
||||
enable = true;
|
||||
autoStart = false; # Start only when requested with just vr.
|
||||
|
||||
# Use the unmodified nixpkgs package without installing its standalone GUI.
|
||||
# This reuses the upstream build; it does not trim Qt from its dependency closure.
|
||||
package = pkgs.symlinkJoin {
|
||||
name = "wivrn-cli-${pkgs.wivrn.version}";
|
||||
inherit (pkgs.wivrn) version meta;
|
||||
paths = [ pkgs.wivrn ];
|
||||
postBuild = ''
|
||||
rm "$out/bin/wivrn-dashboard" "$out/bin/.wivrn-dashboard-wrapped"
|
||||
rm "$out/share/applications/"*.desktop
|
||||
test -x "$out/bin/wivrn-server"
|
||||
test -x "$out/bin/wivrnctl"
|
||||
test ! -e "$out/bin/wivrn-dashboard"
|
||||
for entry in "$out/share/applications/"*.desktop; do
|
||||
test ! -e "$entry"
|
||||
done
|
||||
'';
|
||||
};
|
||||
|
||||
# CAP_SYS_NICE for the server. Games need their own PRIME offload: WiVRn
|
||||
# starts them in separate units, which do not inherit this environment.
|
||||
highPriority = true;
|
||||
steam.importOXRRuntimes = true;
|
||||
};
|
||||
|
||||
# Wait for the control interface used by wivrnctl before reporting success.
|
||||
systemd.user.services.wivrn.serviceConfig = {
|
||||
Type = "dbus";
|
||||
BusName = "io.github.wivrn.Server";
|
||||
TimeoutStartSec = "30s";
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,22 @@
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# prime.offload keeps the Intel iGPU as the default renderer so the dGPU stays
|
||||
# free for LLM workloads, so each consumer that should use the RTX 4090 opts
|
||||
# into PRIME render offload explicitly. Games need these variables in their
|
||||
# Steam launch options: Steam's bubblewrap sandbox does not expose /run, so
|
||||
# the nvidia-offload wrapper on PATH is unusable there. WiVRn's server is a
|
||||
# headless Vulkan app and needs the same environment; headset-launched games
|
||||
# start in separate units or an already running Steam client, so they need
|
||||
# the same per-game launch option (see hosts/idols-ai/VR.md).
|
||||
dgpuEnv = {
|
||||
__NV_PRIME_RENDER_OFFLOAD = "1";
|
||||
__NV_PRIME_RENDER_OFFLOAD_PROVIDER = "NVIDIA-G0";
|
||||
__GLX_VENDOR_LIBRARY_NAME = "nvidia";
|
||||
__VK_LAYER_NV_optimus = "NVIDIA_only";
|
||||
};
|
||||
in
|
||||
{
|
||||
# ===============================================================================================
|
||||
# for Nvidia GPU
|
||||
@@ -22,16 +38,11 @@
|
||||
nvidiaBusId = "PCI:2@0:0:0";
|
||||
};
|
||||
|
||||
# Games launched through GameScope should use the dGPU. prime.offload keeps the
|
||||
# Intel iGPU as the default renderer so the dGPU stays free for LLM workloads,
|
||||
# so opt GameScope games into PRIME render offload here. Games launched outside
|
||||
# GameScope need `nvidia-offload %command%` in their Steam launch options.
|
||||
programs.gamescope.env = {
|
||||
__NV_PRIME_RENDER_OFFLOAD = "1";
|
||||
__NV_PRIME_RENDER_OFFLOAD_PROVIDER = "NVIDIA-G0";
|
||||
__GLX_VENDOR_LIBRARY_NAME = "nvidia";
|
||||
__VK_LAYER_NV_optimus = "NVIDIA_only";
|
||||
};
|
||||
programs.gamescope.env = dgpuEnv;
|
||||
|
||||
# Apply offload to the WiVRn server (see ai/vr.nix). Games need their own
|
||||
# offload configuration; this service environment does not propagate to them.
|
||||
services.wivrn.monadoEnvironment = dgpuEnv;
|
||||
|
||||
boot.kernelParams = [
|
||||
# Since NVIDIA does not load kernel mode setting by default,
|
||||
|
||||
@@ -304,6 +304,16 @@ in
|
||||
".local/share/umu"
|
||||
|
||||
".local/share/Steam"
|
||||
# WiVRn atomically replaces its settings and pairing files, so preserve
|
||||
# the directory rather than individual files.
|
||||
{
|
||||
directory = ".config/wivrn";
|
||||
mode = "0700";
|
||||
}
|
||||
{
|
||||
directory = ".android"; # ADB host keys for Quest USB authorization
|
||||
mode = "0700";
|
||||
}
|
||||
".local/state/Heroic"
|
||||
".config/heroic"
|
||||
|
||||
|
||||
@@ -7,4 +7,14 @@
|
||||
sunshineUsesWlrCapture = "wlr";
|
||||
sunshineStartsAfterNiri = true;
|
||||
sunshineWaitsForNiriOutput = true;
|
||||
|
||||
wivrnEnabled = true;
|
||||
wivrnAutoStarts = false;
|
||||
wivrnHighPriority = true;
|
||||
wivrnWaitsForBus = true;
|
||||
wivrnImportsOpenXRToSteam = true;
|
||||
wivrnUsesNvidiaVulkan = true;
|
||||
wivrnSharesDgpuEnvWithGamescope = true;
|
||||
wivrnStatePreserved = true;
|
||||
adbStatePreserved = true;
|
||||
}
|
||||
@@ -6,6 +6,12 @@
|
||||
let
|
||||
cfg = outputs.nixosConfigurations.ai-niri.config;
|
||||
niriHardware = builtins.readFile ../../../../hosts/idols-ai/niri-hardware.kdl;
|
||||
preserved = cfg.preservation.preserveAt."/persistent".users.ryan.directories;
|
||||
privatelyPreserved =
|
||||
path:
|
||||
lib.any (
|
||||
d: d.directory == cfg.users.users.ryan.home + "/" + path && d.how == "bindmount" && d.mode == "0700"
|
||||
) preserved;
|
||||
in
|
||||
{
|
||||
loadsVirtualDisplay = builtins.elem "vkms" cfg.boot.kernelModules;
|
||||
@@ -16,4 +22,19 @@ in
|
||||
sunshineUsesWlrCapture = cfg.services.sunshine.settings.capture;
|
||||
sunshineStartsAfterNiri = builtins.elem "niri.service" cfg.systemd.user.services.sunshine.after;
|
||||
sunshineWaitsForNiriOutput = lib.hasInfix "niri msg outputs" cfg.systemd.user.services.sunshine.preStart;
|
||||
|
||||
wivrnEnabled = cfg.services.wivrn.enable;
|
||||
wivrnAutoStarts = builtins.elem "default.target" cfg.systemd.user.services.wivrn.wantedBy;
|
||||
wivrnHighPriority = cfg.services.wivrn.highPriority;
|
||||
wivrnWaitsForBus =
|
||||
cfg.systemd.user.services.wivrn.serviceConfig.Type == "dbus"
|
||||
&& cfg.systemd.user.services.wivrn.serviceConfig.BusName == "io.github.wivrn.Server"
|
||||
&& cfg.systemd.user.services.wivrn.serviceConfig.TimeoutStartSec == "30s";
|
||||
wivrnImportsOpenXRToSteam =
|
||||
cfg.environment.sessionVariables.PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES == "1";
|
||||
wivrnUsesNvidiaVulkan = cfg.services.wivrn.monadoEnvironment.__VK_LAYER_NV_optimus == "NVIDIA_only";
|
||||
wivrnSharesDgpuEnvWithGamescope =
|
||||
cfg.services.wivrn.monadoEnvironment == cfg.programs.gamescope.env;
|
||||
wivrnStatePreserved = privatelyPreserved ".config/wivrn";
|
||||
adbStatePreserved = privatelyPreserved ".android";
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env nu
|
||||
#
|
||||
# Route desktop audio to the WiVRn headset, and back again.
|
||||
#
|
||||
# WiVRn creates its speaker (wivrn.sink) and microphone (wivrn.source) only
|
||||
# while a headset is connected, so this script waits (bounded) for those nodes
|
||||
# before it switches the default devices and moves the streams that are already
|
||||
# playing. The previous defaults are saved under $XDG_RUNTIME_DIR, so they last
|
||||
# for the login session only; WirePlumber's persisted defaults stay untouched.
|
||||
#
|
||||
# just vr start the service, then route audio to the headset
|
||||
# just vr-audio route audio after a headset connected later
|
||||
# just vr-stop restore the previous devices, then stop the service
|
||||
#
|
||||
# See hosts/idols-ai/VR.md.
|
||||
|
||||
# WiVRn's PipeWire node names (server/driver/wivrn_session.cpp).
|
||||
const SINK = "wivrn.sink"
|
||||
const SOURCE = "wivrn.source"
|
||||
|
||||
# 'pactl list' output for a target ("sinks", "sources", "sink-inputs", ...).
|
||||
def list-devices [target: string] {
|
||||
(^pactl --format=json list $target | complete | get stdout | from json)
|
||||
}
|
||||
|
||||
# The device called $name, or null.
|
||||
def find-device [target: string, name: string] {
|
||||
let matches = (list-devices $target | where name == $name)
|
||||
if ($matches | is-empty) { null } else { $matches | get 0 }
|
||||
}
|
||||
|
||||
# Wait for $name to appear, up to $seconds; returns null on timeout.
|
||||
def wait-for-device [target: string, name: string, seconds: int] {
|
||||
let deadline = (date now) + ($seconds * 1sec)
|
||||
mut waited = 0
|
||||
loop {
|
||||
let device = (find-device $target $name)
|
||||
if $device != null { return $device }
|
||||
if (date now) > $deadline { return null }
|
||||
sleep 2sec
|
||||
$waited += 2
|
||||
if ($waited mod 30) == 0 { print $"audio: still waiting for the headset (($waited)s)" }
|
||||
}
|
||||
}
|
||||
|
||||
# Where the previous defaults are remembered (per login session).
|
||||
def state-path [] {
|
||||
let runtime = ($env.XDG_RUNTIME_DIR? | default $"/run/user/(^id -u | str trim)")
|
||||
$runtime | path join "wivrn-audio.json"
|
||||
}
|
||||
|
||||
# The current default sink or source name.
|
||||
def default-device [kind: string] {
|
||||
(^pactl $"get-default-($kind)" | complete | get stdout | str trim)
|
||||
}
|
||||
|
||||
# Point the default sink or source at $name.
|
||||
def set-default-device [kind: string, name: string] {
|
||||
^pactl $"set-default-($kind)" $name | complete | ignore
|
||||
}
|
||||
|
||||
# Move every playback stream to $sink.
|
||||
def move-sink-inputs [sink: string] {
|
||||
for input in (list-devices "sink-inputs") {
|
||||
^pactl move-sink-input ($input.index | into string) $sink | complete | ignore
|
||||
}
|
||||
}
|
||||
|
||||
# Move every recording stream to $source.
|
||||
def move-source-outputs [source: string] {
|
||||
for output in (list-devices "source-outputs") {
|
||||
^pactl move-source-output ($output.index | into string) $source | complete | ignore
|
||||
}
|
||||
}
|
||||
|
||||
# Point the default devices at the headset and follow with current streams.
|
||||
def switch [seconds: int] {
|
||||
print $"audio: waiting up to ($seconds)s for the headset to connect"
|
||||
let sink = (wait-for-device "sinks" $SINK $seconds)
|
||||
if $sink == null {
|
||||
print $"audio: the headset did not connect within ($seconds)s; run 'just vr-audio' once it does"
|
||||
return
|
||||
}
|
||||
|
||||
let state = (state-path)
|
||||
if (default-device "sink") != $SINK {
|
||||
{ sink: (default-device "sink"), source: (default-device "source") } | to json | save -f $state
|
||||
}
|
||||
|
||||
set-default-device "sink" $SINK
|
||||
move-sink-inputs $SINK
|
||||
print $"audio: playback -> ($SINK), previous default saved in ($state)"
|
||||
|
||||
let source = (find-device "sources" $SOURCE)
|
||||
if $source != null {
|
||||
set-default-device "source" $SOURCE
|
||||
move-source-outputs $SOURCE
|
||||
print $"audio: microphone -> ($SOURCE)"
|
||||
} else {
|
||||
print $"audio: no ($SOURCE) yet, microphone left alone"
|
||||
}
|
||||
}
|
||||
|
||||
# Put the remembered default devices back.
|
||||
def restore [] {
|
||||
let state = (state-path)
|
||||
if not ($state | path exists) {
|
||||
print "audio: no saved defaults, nothing to restore"
|
||||
return
|
||||
}
|
||||
|
||||
let previous = (open --raw $state | from json)
|
||||
rm $state
|
||||
|
||||
if (find-device "sinks" $previous.sink) == null {
|
||||
print $"audio: ($previous.sink) is gone, leaving the default to WirePlumber"
|
||||
} else {
|
||||
set-default-device "sink" $previous.sink
|
||||
move-sink-inputs $previous.sink
|
||||
print $"audio: playback -> ($previous.sink)"
|
||||
}
|
||||
|
||||
if (find-device "sources" $previous.source) == null {
|
||||
print $"audio: ($previous.source) is gone, leaving the default to WirePlumber"
|
||||
} else {
|
||||
set-default-device "source" $previous.source
|
||||
move-source-outputs $previous.source
|
||||
print $"audio: microphone -> ($previous.source)"
|
||||
}
|
||||
}
|
||||
|
||||
# Entry point: 'nu wivrn-audio.nu switch' / '... restore'.
|
||||
def main [
|
||||
action: string = "switch" # switch to the headset, or restore the previous devices
|
||||
--timeout: int = 120 # seconds to wait for the headset audio devices
|
||||
] {
|
||||
match $action {
|
||||
"switch" => { switch $timeout }
|
||||
"restore" => { restore }
|
||||
_ => { error make { msg: $"unknown action '($action)'; expected switch or restore" } }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user