This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tablet-screen: android tablet as external screen via sunshine

Streams a sway headless output to Moonlight with Sunshine, so an Android
tablet works as a wireless second screen with its touchscreen acting as a
touchpad on the shared cursor.

The output is created once per session and stays disabled until a client
streams: sunshine's prep-cmd enables it at exactly the resolution and
refresh rate the client asks for, and disables it afterwards so sway moves
the workspace back to the real screens. wlroots never reuses a headless
output name, so the name is discovered at creation time, remembered in
$XDG_RUNTIME_DIR and handed to sunshine as an output_name command line
override instead of being hardcoded.

Replaces 2configs/sunshine.nix, which only shipped a manual launcher
script.

+241 -29
-28
2configs/sunshine.nix
··· 1 - { pkgs, ... }: 2 - { 3 - networking.firewall.allowedTCPPorts = [ 4 - 47984 5 - 47989 6 - 48010 7 - ]; 8 - networking.firewall.allowedUDPPorts = [ 9 - 47998 10 - 47999 11 - 48000 12 - ]; 13 - 14 - boot.kernelModules = [ "uinput" ]; 15 - services.udev.extraRules = '' 16 - KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" 17 - ''; 18 - 19 - environment.systemPackages = [ 20 - (pkgs.writers.writeDashBin "sun" '' 21 - ${pkgs.sunshine}/bin/sunshine --creds sun sun 22 - ${pkgs.sunshine}/bin/sunshine -0 ${pkgs.writeText "sunshine.conf" '' 23 - channels = 15 24 - address_family = both 25 - ''} "$@" 26 - '') 27 - ]; 28 - }
+240
2configs/tablet-screen.nix
··· 1 + # Android tablet as a wireless external screen. 2 + # 3 + # A sway headless output is streamed to Moonlight by Sunshine: hardware H.264/ 4 + # HEVC/AV1 over UDP, i.e. the game streaming path, ~1 frame of latency on LAN. 5 + # The output exists but stays disabled until a client streams: Sunshine's 6 + # prep-cmd enables it at exactly the resolution/refresh rate the client asks 7 + # for (so pixels map 1:1) and disables it afterwards, which makes sway evacuate 8 + # its workspace back to the real screens. 9 + # 10 + # Touch as a touchpad: Moonlight's "Use the touchscreen as a trackpad" sends 11 + # relative mouse motion, which Sunshine injects through uinput, so dragging 12 + # anywhere on the tablet moves the (single, shared) cursor like a touchpad. 13 + # native_pen_touch is disabled below so absolute touch is never negotiated 14 + # instead. 15 + # 16 + # One-time setup per host: 17 + # sunshine --creds <user> <password> # or set them in the web UI 18 + # open https://<host>:47990, "PIN" page, enter the PIN Moonlight shows 19 + # 20 + # For the least judder, use 5 GHz, keep tablet and host on the same AP, and in 21 + # Moonlight pick HEVC with frame pacing on "Prefer lowest latency". Streaming 22 + # jitter is dominated by the tablet's radio, not by which host NIC is used. 23 + { 24 + config, 25 + lib, 26 + pkgs, 27 + ... 28 + }: 29 + let 30 + workspace = "tablet"; 31 + 32 + tablet-screen = pkgs.writeShellApplication { 33 + name = "tablet-screen"; 34 + runtimeInputs = [ 35 + pkgs.sway 36 + pkgs.jq 37 + ]; 38 + text = '' 39 + # wlroots names headless outputs HEADLESS-<n> in creation order and never 40 + # reuses a name, so the name we get depends on what else created one 41 + # first. Reserve one per session and remember which it is; the state lives 42 + # in the runtime dir, so it disappears together with the sway session that 43 + # owns the output. 44 + state=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/tablet-screen.output 45 + 46 + # sway >=1.11 creates a per-pid IPC socket, and the systemd user manager 47 + # has not necessarily imported SWAYSOCK yet when sunshine starts or runs a 48 + # prep-cmd, so find the live socket ourselves. 49 + if ! swaymsg -t get_version >/dev/null 2>&1; then 50 + for sock in /run/user/"$(id -u)"/sway-ipc.*.sock; do 51 + if SWAYSOCK=$sock swaymsg -t get_version >/dev/null 2>&1; then 52 + export SWAYSOCK=$sock 53 + break 54 + fi 55 + done 56 + fi 57 + 58 + names() { 59 + swaymsg -r -t get_outputs | jq -c '[.[].name]' 60 + } 61 + 62 + exists() { 63 + names | jq -e --arg o "$1" 'index($o)' >/dev/null 64 + } 65 + 66 + reserve() { 67 + if [ -s "$state" ] && exists "$(cat "$state")"; then 68 + return 0 69 + fi 70 + before=$(names) 71 + swaymsg -- create_output >/dev/null 72 + output=$(names | jq -r --argjson before "$before" '(. - $before)[0] // empty') 73 + if [ -z "$output" ]; then 74 + echo "tablet-screen: sway created no headless output" >&2 75 + return 1 76 + fi 77 + printf '%s\n' "$output" > "$state" 78 + # Keep the tablet on its own workspace so it never steals a numbered one. 79 + swaymsg "workspace ${workspace} output $output" >/dev/null 80 + swaymsg "output $output disable" >/dev/null 81 + } 82 + 83 + target() { 84 + reserve 85 + cat "$state" 86 + } 87 + 88 + case "''${1-}" in 89 + reserve) 90 + reserve 91 + ;; 92 + name) 93 + target 94 + ;; 95 + on) 96 + shift 97 + # Sunshine exports the client's stream request; a resolution given on 98 + # the command line wins so the screen can also be used by hand. 99 + res=''${SUNSHINE_CLIENT_WIDTH:-1920}x''${SUNSHINE_CLIENT_HEIGHT:-1200} 100 + fps=''${SUNSHINE_CLIENT_FPS:-60} 101 + scale=1 102 + while [ $# -gt 0 ]; do 103 + case $1 in 104 + --mode) 105 + res=''${2%%@*} 106 + [ "''${2#*@}" = "$2" ] || fps=''${2#*@} 107 + shift 2 108 + ;; 109 + --scale) 110 + scale=$2 111 + shift 2 112 + ;; 113 + *) 114 + echo "tablet-screen: unknown option $1" >&2 115 + exit 1 116 + ;; 117 + esac 118 + done 119 + output=$(target) 120 + # No position: sway appends an unpositioned output to the right of 121 + # every other one, which keeps the existing screens (and the order 122 + # sway-screen-switch counts in) exactly where they are. 123 + swaymsg "output $output enable mode ''${res}@''${fps}Hz scale $scale" >/dev/null 124 + ;; 125 + off) 126 + # Disable instead of unplug: sway moves the workspace back to a real 127 + # screen, and the output keeps its name for the next stream. 128 + if [ -s "$state" ] && exists "$(cat "$state")"; then 129 + swaymsg "output $(cat "$state") disable" >/dev/null 130 + fi 131 + ;; 132 + status) 133 + if ! { [ -s "$state" ] && exists "$(cat "$state")"; }; then 134 + echo "no virtual screen reserved" 135 + exit 0 136 + fi 137 + swaymsg -r -t get_outputs | jq -r --arg o "$(cat "$state")" ' 138 + .[] | select(.name == $o) 139 + | if .active then 140 + "\(.name) active \(.current_mode.width)x\(.current_mode.height)@\((.current_mode.refresh / 1000) | floor) scale=\(.scale) position=\(.rect.x),\(.rect.y)" 141 + else 142 + "\(.name) disabled" 143 + end 144 + ' 145 + ;; 146 + *) 147 + echo "usage: tablet-screen reserve|name|on [--mode WxH[@FPS]] [--scale S]|off|status" >&2 148 + exit 1 149 + ;; 150 + esac 151 + ''; 152 + }; 153 + 154 + appsFile = (pkgs.formats.json { }).generate "sunshine-apps.json" { 155 + env = { }; 156 + apps = [ 157 + { 158 + name = "Tablet screen"; 159 + prep-cmd = [ 160 + { 161 + do = "${lib.getExe tablet-screen} on"; 162 + undo = "${lib.getExe tablet-screen} off"; 163 + } 164 + ]; 165 + auto-detach = "true"; 166 + } 167 + { 168 + # Same screen rendered at 1.5x, for reading size on a ~10" panel while 169 + # still encoding (and displaying) native pixels. 170 + name = "Tablet screen (HiDPI)"; 171 + prep-cmd = [ 172 + { 173 + do = "${lib.getExe tablet-screen} on --scale 1.5"; 174 + undo = "${lib.getExe tablet-screen} off"; 175 + } 176 + ]; 177 + auto-detach = "true"; 178 + } 179 + ]; 180 + }; 181 + 182 + settingsFile = (pkgs.formats.keyValue { }).generate "sunshine.conf" { 183 + sunshine_name = config.networking.hostName; 184 + # wlr-screencopy, unlike the kms backend, can also capture an output that 185 + # has no CRTC behind it - which is exactly what a headless output is. 186 + capture = "wlr"; 187 + # Make Moonlight emulate a mouse rather than negotiating absolute touch, 188 + # so the touchscreen acts as a touchpad on the shared cursor. 189 + native_pen_touch = "disabled"; 190 + file_apps = "${appsFile}"; 191 + }; 192 + 193 + # output_name has to be known when sunshine starts, but the headless output's 194 + # name is only decided when it is created, so reserve it here and pass the 195 + # name as a command line override (any config key can be overridden that way). 196 + # Running through the capability wrapper gives sunshine CAP_SYS_NICE for its 197 + # EGL capture context. 198 + sunshine-tablet-screen = pkgs.writeShellApplication { 199 + name = "sunshine-tablet-screen"; 200 + runtimeInputs = [ tablet-screen ]; 201 + text = '' 202 + output=$(tablet-screen name) 203 + exec ${config.security.wrapperDir}/sunshine ${settingsFile} "output_name=$output" 204 + ''; 205 + }; 206 + in 207 + { 208 + environment.systemPackages = [ tablet-screen ]; 209 + 210 + services.sunshine = { 211 + enable = true; 212 + openFirewall = true; 213 + # Started with sway-session.target below instead, which sway only reaches 214 + # after importing WAYLAND_DISPLAY/SWAYSOCK into the user manager. 215 + autoStart = false; 216 + # Not for KMS capture (unused): this is what makes the module build a 217 + # capability wrapper, whose capability set is replaced below. 218 + capSysAdmin = true; 219 + }; 220 + 221 + # CAP_SYS_NICE lets sunshine put its EGL capture context on a high priority 222 + # GPU queue; CAP_SYS_ADMIN would only be needed for KMS capture. 223 + security.wrappers.sunshine.capabilities = lib.mkForce "cap_sys_nice+p"; 224 + 225 + systemd.user.services.sunshine = { 226 + wantedBy = [ "sway-session.target" ]; 227 + partOf = [ "sway-session.target" ]; 228 + after = [ "sway-session.target" ]; 229 + # Fails while sway is not up yet (no IPC socket to create the output on); 230 + # the module's Restart=on-failure retries until the session exists. 231 + serviceConfig.ExecStart = lib.mkForce (lib.getExe sunshine-tablet-screen); 232 + }; 233 + 234 + # Mouse/keyboard injection goes through /dev/uinput; hand it to the user of 235 + # the active session (hardware.uinput, pulled in by services.sunshine, only 236 + # sets up the uinput group). 237 + services.udev.extraRules = '' 238 + KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" 239 + ''; 240 + }
+1 -1
machines/ignavia/config.nix
··· 37 37 ../../2configs/container-tests.nix 38 38 ../../2configs/rad.nix 39 39 ../../2configs/herdr.nix 40 - # ../../2configs/sunshine.nix 40 + ../../2configs/tablet-screen.nix 41 41 # ../../2configs/br.nix 42 42 # ../../2configs/c-base.nix 43 43 ];