Add headless streaming mode + Mac client + full docs
Headless mode (new) — for KVM-attached hosts streaming to disconnected clients - --headless / --mirror flags; default headless on hostname JARVIS, mirror elsewhere - New lib/headless.sh installs prep-cmd hooks to ~/.local/share/omarchy-moonlight/bin - bin/sunshine-stream-do.sh creates/resizes a Hyprland HEADLESS-1 output to the connecting client's resolution and migrates the active workspace onto it - bin/sunshine-stream-undo.sh tears down the headless output on disconnect and returns the workspace to a non-headless monitor when one is available - lib/config.sh writes capture=wlr, output_name=HEADLESS-1, and the JSON global_prep_cmd entry referencing the installed hook paths - lib/preflight.sh adds a preflight_headless step that checks hyprctl, jq, and a running Hyprland session (warn-only, install can proceed) - lib/verify.sh adds checks for the hook scripts and the wlr/global_prep_cmd config lines Mac client - client/install-macos.sh: Darwin guard, Homebrew presence check, brew cask install of Moonlight, idempotent - client/README.md: per-platform install (macOS / Android / iOS / Apple TV / Linux + Steam Deck) and the five-step first-pair walkthrough Other - jq added to the helper install set in lib/packages.sh (hooks parse Hyprland JSON output) - README.md rewritten to cover both modes, the new flags, the tuned defaults per mode + per vendor, the headless internals, and the client pointer
This commit is contained in:
214
README.md
214
README.md
@@ -1,14 +1,8 @@
|
|||||||
# omarchy-moonlight
|
# omarchy-moonlight
|
||||||
|
|
||||||
Idempotent install scripts that set up [Sunshine](https://github.com/LizardByte/Sunshine) (host) and [Moonlight](https://moonlight-stream.org/) (client) on Omarchy / Arch Linux / Hyprland / Wayland machines.
|
Idempotent bash installer that sets up [Sunshine](https://github.com/LizardByte/Sunshine) (host) and [Moonlight](https://moonlight-stream.org/) (client) on Omarchy / Arch Linux / Hyprland / Wayland machines. Works across NVIDIA, AMD, and Intel hosts. A companion script handles the Mac client; iOS / Android / Apple TV clients are App Store installs.
|
||||||
|
|
||||||
Designed so the same script runs on:
|
The same script can leave a machine acting as host, client, or both. Re-running it is safe: every step is "check, then act."
|
||||||
|
|
||||||
- a primary NVIDIA desktop (host + client)
|
|
||||||
- a Framework AMD laptop (host + client)
|
|
||||||
- any other Omarchy box
|
|
||||||
|
|
||||||
After install, the machine can both stream out (via Sunshine) and view streams (via Moonlight). A Macbook can install Moonlight separately and connect to either Linux host.
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -20,69 +14,119 @@ cd ~/omarchy-moonlight
|
|||||||
|
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
1. **Log out and back in** if you weren't already in the `input` group (the installer adds you; the group only takes effect on a fresh login).
|
1. **Log out and back in** if you weren't already in the `input` group (the installer adds you; the group only takes effect on a fresh login). `newgrp input` works as a one-shell shortcut.
|
||||||
2. Open <https://localhost:47990> and set a Sunshine username + password.
|
2. Open <https://localhost:47990> and set a Sunshine username + password. The cert is self-signed; accept the warning.
|
||||||
3. On a Moonlight client (Mac / phone / the other Linux box), add this host by LAN IP and enter the PIN that Sunshine's web UI shows during pairing.
|
3. On a Moonlight client (Mac / phone / the other Linux box), add this host by LAN IP and enter the 4-digit PIN that Sunshine's web UI shows during pairing.
|
||||||
|
|
||||||
## What it does
|
## Streaming modes
|
||||||
|
|
||||||
In order, each step is "check, then act" — re-running is safe:
|
The installer picks between two capture strategies. The choice gets baked into `~/.config/sunshine/sunshine.conf` and (for headless) wires in two prep-cmd hook scripts.
|
||||||
|
|
||||||
1. **Preflight** — confirms Wayland session, GPU driver is loaded, `nvidia-drm.modeset=1` isn't explicitly disabled (would silently break KMS capture), pipewire-pulse is present for audio.
|
| Mode | Capture backend | Stream resolution | Needs physical monitor? | Default on |
|
||||||
2. **Packages** — installs `sunshine-bin` (precompiled, fast) and `moonlight-qt` from the AUR via `yay`. Plus runtime helpers: `pipewire-pulse`, `vulkan-tools`, `libva-utils`. Use `--from-source` to build Sunshine from source instead.
|
|---|---|---|---|---|
|
||||||
|
| **Mirror** | `capture=kms` (DRM) | Whatever the host's monitor is set to | Yes (or a connected dummy plug) | All hosts except `JARVIS` |
|
||||||
|
| **Headless** | `capture=wlr`, `output_name=HEADLESS-1` | Adapts per-connection to each client's native resolution | No | Hostname `JARVIS` |
|
||||||
|
|
||||||
|
Override with `--headless` or `--mirror` on any host.
|
||||||
|
|
||||||
|
### Mirror mode
|
||||||
|
|
||||||
|
Sunshine captures the real DRM display. Lowest latency, simplest mental model. The client sees exactly what's on the host monitor. Pick this when the host has a working display attached and you want a "remote screen" experience.
|
||||||
|
|
||||||
|
### Headless mode
|
||||||
|
|
||||||
|
Sunshine captures a Hyprland headless output. On each client connect, a `global_prep_cmd` runs `sunshine-stream-do.sh`, which:
|
||||||
|
|
||||||
|
1. Reads `SUNSHINE_CLIENT_WIDTH`, `SUNSHINE_CLIENT_HEIGHT`, `SUNSHINE_CLIENT_FPS` from Sunshine's environment.
|
||||||
|
2. Ensures `HEADLESS-1` exists, creating it via `hyprctl output create headless` if missing.
|
||||||
|
3. Resizes it to the client's native resolution / framerate via `hyprctl keyword monitor`.
|
||||||
|
4. Moves the currently active workspace onto `HEADLESS-1` and focuses it, so existing windows appear in the stream.
|
||||||
|
|
||||||
|
On disconnect, `sunshine-stream-undo.sh` moves the workspace back to a real monitor (if one exists) and removes `HEADLESS-1`.
|
||||||
|
|
||||||
|
The result: different clients can connect at different resolutions and the host adapts per-connection. The host's physical monitor is optional — perfect for a KVM-attached machine whose monitor is often switched to another input. The hostname-`JARVIS` default exists because that's the canonical KVM-attached target; a laptop with its own screen is better off in mirror mode.
|
||||||
|
|
||||||
|
## What the installer does
|
||||||
|
|
||||||
|
Every step is idempotent. In order:
|
||||||
|
|
||||||
|
1. **Preflight** — confirms Wayland session, GPU driver is loaded, `nvidia-drm.modeset=1` is not explicitly disabled (would silently break KMS capture), `pipewire-pulse` is present for audio.
|
||||||
|
2. **Packages** — installs `sunshine-bin` (precompiled) and `moonlight-qt` from the AUR via `yay`, plus runtime helpers: `pipewire-pulse`, `vulkan-tools`, `libva-utils`. `--from-source` (or `SUNSHINE_PKG=sunshine`) switches to the source build.
|
||||||
3. **GPU encoder support**:
|
3. **GPU encoder support**:
|
||||||
- NVIDIA: `nvidia-utils`, `libva-nvidia-driver`
|
- NVIDIA: `nvidia-utils`, `libva-nvidia-driver`
|
||||||
- AMD: `libva-mesa-driver`, `mesa-vdpau`, `vulkan-radeon`
|
- AMD: `libva-mesa-driver`, `mesa-vdpau`, `vulkan-radeon`
|
||||||
- Intel: `intel-media-driver`, `vulkan-intel`
|
- Intel: `intel-media-driver`, `vulkan-intel`
|
||||||
4. **Permissions**:
|
4. **Permissions**:
|
||||||
- Adds you to the `input` group
|
- Adds you to the `input` group.
|
||||||
- Drops `/etc/udev/rules.d/60-uinput.rules` if no equivalent rule exists (lets Sunshine use `/dev/uinput` for virtual gamepad/keyboard/mouse)
|
- Drops `/etc/udev/rules.d/60-uinput.rules` if no equivalent rule exists (so Sunshine can use `/dev/uinput` for virtual gamepad / keyboard / mouse).
|
||||||
- `setcap cap_sys_admin+p` on the `sunshine` binary so KMS screen capture works without root
|
- `setcap cap_sys_admin+p` on the `sunshine` binary so KMS screen capture works without root.
|
||||||
5. **Tuned config** — writes `~/.config/sunshine/sunshine.conf` for low-latency LAN streaming. Per-vendor encoder settings (NVENC P1+`ll`, VAAPI ultralowlatency, QuickSync veryfast). Marked with `# managed-by: omarchy-moonlight`; remove that marker to take ownership and the installer will never touch it again.
|
5. **Headless hooks** (headless mode only) — installs `sunshine-stream-do.sh` and `sunshine-stream-undo.sh` to `~/.local/share/omarchy-moonlight/bin/` and references them as the Sunshine `global_prep_cmd`.
|
||||||
6. **Firewall** — opens Sunshine's ports on `firewalld` / `ufw` if either is active. Skips silently otherwise.
|
6. **Tuned config** — writes `~/.config/sunshine/sunshine.conf` for low-latency LAN streaming, with per-vendor encoder picks. Marked with `# managed-by: omarchy-moonlight`; remove that marker to take ownership and the installer will never touch it again.
|
||||||
7. **Service** — enables `sunshine.service` under `systemd --user` and turns on `loginctl enable-linger` so the host is reachable without an active graphical login.
|
7. **Firewall** — opens Sunshine's ports on `firewalld` / `ufw` if either is active. Skips silently otherwise.
|
||||||
8. **Verify** — runs the same checks as `--doctor` to confirm everything's actually wired up (cap_sys_admin set, group resolved, web UI listening on :47990, encoder reachable).
|
8. **Service** — enables `sunshine.service` under `systemctl --user` and turns on `loginctl enable-linger` so the host is reachable without an active graphical login.
|
||||||
|
9. **Verify** — runs the same checks as `--doctor` to confirm everything's actually wired up (cap_sys_admin set, group resolved, web UI listening on `:47990`, encoder reachable, hooks present where expected).
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
```text
|
```text
|
||||||
./install.sh --no-autostart # install but don't enable the user service
|
./install.sh # auto-detect mode (headless on JARVIS, mirror elsewhere)
|
||||||
./install.sh --no-firewall # skip firewall rules
|
./install.sh --headless # force headless mode
|
||||||
./install.sh --no-moonlight # host-only (no client)
|
./install.sh --mirror # force mirror mode
|
||||||
./install.sh --no-sunshine # client-only (no host)
|
./install.sh --no-autostart # install but don't enable systemctl --user sunshine
|
||||||
./install.sh --no-config # leave Sunshine to generate its own default config
|
./install.sh --no-firewall # skip firewall configuration
|
||||||
./install.sh --from-source # build Sunshine from source (slower; uses 'sunshine' AUR pkg)
|
./install.sh --no-config # don't write a tuned sunshine.conf
|
||||||
./install.sh --doctor # run only the verification checks (no install)
|
./install.sh --no-sunshine # client-only (install Moonlight only)
|
||||||
|
./install.sh --no-moonlight # host-only
|
||||||
|
./install.sh --from-source # build sunshine from source (default uses sunshine-bin)
|
||||||
|
./install.sh --doctor # verification only (no install)
|
||||||
|
|
||||||
|
./uninstall.sh # remove packages and udev rule, keep user data
|
||||||
|
./uninstall.sh --purge # also delete ~/.config/sunshine
|
||||||
|
./uninstall.sh --keep-moonlight
|
||||||
```
|
```
|
||||||
|
|
||||||
The doctor flag is the fastest way to debug a degraded install — it'll tell you exactly which piece (group, cap, udev, encoder, service, port) is broken.
|
Environment overrides:
|
||||||
|
|
||||||
### Tuned defaults written to sunshine.conf
|
| Variable | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `SUNSHINE_PKG=sunshine` | Build from source (equivalent to `--from-source`). |
|
||||||
|
| `SUNSHINE_PKG=sunshine-bin` | Use the prebuilt AUR package (default). |
|
||||||
|
|
||||||
| Setting | Value | Why |
|
The `--doctor` flag is the fastest way to debug a degraded install — it reports exactly which piece (group, cap, udev, encoder, service, port, hooks) is broken.
|
||||||
|---|---|---|
|
|
||||||
| `capture` | `kms` | Correct backend for Wayland; uses DRM directly. |
|
|
||||||
| `encoder` (NVIDIA) | `nvenc` + `nvenc_preset=p1`, `nvenc_tune=ll`, `nvenc_rc=cbr` | P1 minimizes encode latency; low-latency tune disables look-ahead; CBR keeps bitrate predictable over LAN. |
|
|
||||||
| `encoder` (AMD) | `vaapi` + `amd_usage=ultralowlatency`, `amd_rc=cbr` | Mirrors the NVIDIA choices on AMD's encoder. |
|
|
||||||
| `min_threads` | `4` | Helps keep up at high bitrates / 4K. |
|
|
||||||
| `audio_sink` | `pulse` | Captures from PipeWire's Pulse compat layer. |
|
|
||||||
|
|
||||||
Anything else (resolution, bitrate, paired clients, app launchers) is set via the web UI.
|
## Tuned defaults
|
||||||
|
|
||||||
## Uninstall
|
Written to `~/.config/sunshine/sunshine.conf` with a `# managed-by: omarchy-moonlight` marker on the first line. The installer only overwrites the file while that marker is present; delete the marker (or the line) to lock the file against future runs.
|
||||||
|
|
||||||
```bash
|
| Setting | Mirror value | Headless value | Why |
|
||||||
./uninstall.sh # remove packages + udev rule, keep user data
|
|---|---|---|---|
|
||||||
./uninstall.sh --purge # also delete ~/.config/sunshine
|
| `capture` | `kms` | `wlr` | KMS for real DRM displays; `wlr` for Hyprland headless outputs. |
|
||||||
```
|
| `output_name` | (unset) | `HEADLESS-1` | Pin wlr capture to the headless output the hooks manage. |
|
||||||
|
| `global_prep_cmd` | (unset) | `do`/`undo` pair | Runs the headless hook scripts on client connect / disconnect. |
|
||||||
|
| `min_threads` | `4` | `4` | Helps keep up at high bitrates / 4K. |
|
||||||
|
| `audio_sink` | `pulse` | `pulse` | Captures from PipeWire's Pulse compat layer. |
|
||||||
|
|
||||||
## How streaming works once it's set up
|
Per-vendor encoder picks:
|
||||||
|
|
||||||
- **Host (Sunshine) ports** (auto-opened if a firewall is active):
|
| Vendor | Encoder | Settings | Why |
|
||||||
- TCP: `47984 47989 47990 48010`
|
|---|---|---|---|
|
||||||
- UDP: `47998 47999 48000 48010`
|
| NVIDIA | `nvenc` | `nvenc_preset=p1`, `nvenc_tune=ll`, `nvenc_rc=cbr` | P1 minimizes encode latency; `ll` disables look-ahead; CBR keeps bitrate predictable over LAN. |
|
||||||
- **Pairing**: on first connect, Moonlight shows a PIN. Type it into Sunshine's web UI (<https://localhost:47990> → PIN tab) within a few seconds.
|
| AMD | `vaapi` | `amd_usage=ultralowlatency`, `amd_rc=cbr` | Mirrors the NVIDIA latency-first picks on AMD's encoder. |
|
||||||
- **Capture mode**: this script configures KMS capture, which streams whatever is on the host's real monitor. A virtual-display mode (so streaming doesn't take over the desk) is a future addition — see `remote/` notes when it lands.
|
| Intel | `quicksync` | `qsv_preset=veryfast` | Lowest-latency QuickSync preset. |
|
||||||
|
|
||||||
|
Everything else (bitrate, paired clients, app launchers) is set via the web UI.
|
||||||
|
|
||||||
|
## Clients
|
||||||
|
|
||||||
|
The host-side installer handles Linux clients via `moonlight-qt`. For everything else, see `client/README.md` for per-platform install plus the first-pair walkthrough.
|
||||||
|
|
||||||
|
| Platform | Install path |
|
||||||
|
|---|---|
|
||||||
|
| Linux (Arch / Omarchy) | Bundled — `./install.sh` installs `moonlight-qt` unless `--no-moonlight`. |
|
||||||
|
| macOS | `client/install-macos.sh` (Moonlight via Homebrew cask). |
|
||||||
|
| iOS / iPadOS | App Store: Moonlight Game Streaming. |
|
||||||
|
| Android | Play Store: Moonlight Game Streaming. |
|
||||||
|
| Apple TV (tvOS) | App Store: Moonlight Game Streaming. |
|
||||||
|
|
||||||
## Diagnostics
|
## Diagnostics
|
||||||
|
|
||||||
@@ -94,16 +138,47 @@ getcap "$(readlink -f "$(command -v sunshine)")" # should include cap_sys_a
|
|||||||
id -nG | tr ' ' '\n' | grep -x input # confirm group membership
|
id -nG | tr ' ' '\n' | grep -x input # confirm group membership
|
||||||
```
|
```
|
||||||
|
|
||||||
If Moonlight pairs but the stream is black:
|
Useful Sunshine ports (auto-opened if a firewall is active):
|
||||||
|
|
||||||
- Confirm you're in the `input` group **in a freshly logged-in session** (not just listed in `/etc/group`).
|
- TCP: `47984 47989 47990 48010`
|
||||||
- Confirm `getcap` shows `cap_sys_admin` on the sunshine binary.
|
- UDP: `47998 47999 48000 48010`
|
||||||
- Check `journalctl --user -u sunshine` for `KMS` / `DRM` errors.
|
|
||||||
- On NVIDIA: confirm the proprietary driver is active (`nvidia-smi`) and that `nvidia-drm.modeset=1` is in effect. If you have an older driver (≤555) and modeset isn't on, add `nvidia-drm.modeset=1` to your kernel cmdline and reboot.
|
## Headless mode internals
|
||||||
|
|
||||||
|
Hooks live at `~/.local/share/omarchy-moonlight/bin/` and are referenced from `sunshine.conf` as a `global_prep_cmd` pair.
|
||||||
|
|
||||||
|
| Script | Trigger | Responsibilities |
|
||||||
|
|---|---|---|
|
||||||
|
| `sunshine-stream-do.sh` | Client connects | Ensure `HEADLESS-1` exists; resize it to `${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}@${SUNSHINE_CLIENT_FPS}`; move active workspace onto it; focus it. |
|
||||||
|
| `sunshine-stream-undo.sh` | Client disconnects | Move workspace back to a real monitor (if any); remove `HEADLESS-1`. |
|
||||||
|
|
||||||
|
Environment variables Sunshine sets on each connect and the hooks consume:
|
||||||
|
|
||||||
|
- `SUNSHINE_CLIENT_WIDTH` — client viewport width in pixels.
|
||||||
|
- `SUNSHINE_CLIENT_HEIGHT` — client viewport height in pixels.
|
||||||
|
- `SUNSHINE_CLIENT_FPS` — client target framerate.
|
||||||
|
|
||||||
|
The hooks defensively recover `HYPRLAND_INSTANCE_SIGNATURE` by scanning `$XDG_RUNTIME_DIR/hypr/` if Sunshine's environment doesn't inherit it (rare, but possible under stripped-down user services). They also snapshot prior monitor state under `$XDG_RUNTIME_DIR/sunshine-headless/` so undo can restore the previous workspace assignment. If Hyprland isn't running, the hooks log a message and exit cleanly — Sunshine still streams, just without per-client resizing.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Stream pairs but is black.** Confirm three things:
|
||||||
|
|
||||||
|
1. You're in the `input` group in a freshly logged-in session (not just listed in `/etc/group` — group membership is set at login). `id -nG` is the truth.
|
||||||
|
2. `getcap` shows `cap_sys_admin+p` on the resolved `sunshine` binary.
|
||||||
|
3. `journalctl --user -u sunshine` doesn't show `KMS` / `DRM` / `wlr` errors at stream start.
|
||||||
|
|
||||||
|
**NVIDIA: capture starts then dies.** Older proprietary drivers (≤555) need `nvidia-drm.modeset=1` on the kernel command line. `cat /sys/module/nvidia_drm/parameters/modeset` should print `Y`. If not, add `nvidia-drm.modeset=1` to your bootloader cmdline and reboot.
|
||||||
|
|
||||||
|
**KVM / headless: client sees an old resolution.** The headless hooks resize `HEADLESS-1` per connect. If you see stale geometry, check `~/.local/share/omarchy-moonlight/bin/sunshine-stream-do.sh` is referenced in `sunshine.conf`'s `global_prep_cmd` and that `hyprctl monitors all` lists `HEADLESS-1` during a stream.
|
||||||
|
|
||||||
|
**Headless host, no monitor at all.** Hyprland needs to be running for the hooks to do anything. On a truly headless box, run Hyprland under `uwsm` from a tty (or via `systemctl --user start hyprland-session.target`) so its IPC socket exists when Sunshine fires the prep command.
|
||||||
|
|
||||||
|
**Moonlight can't find the host on the LAN.** Confirm the firewall ports above are open. mDNS discovery is best-effort; adding the host manually by LAN IP is the reliable path.
|
||||||
|
|
||||||
## Remote access (planned)
|
## Remote access (planned)
|
||||||
|
|
||||||
LAN-only for now. Remote access will be added later via one of: Tailscale, WireGuard, or Cloudflare. See `remote/` (stub) when implemented.
|
LAN-only for now. Remote access is planned via one of Tailscale, WireGuard, or Cloudflare Tunnel, depending on which gives the best UDP latency to mobile clients. Notes will land under `remote/` when implemented.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -112,15 +187,22 @@ omarchy-moonlight/
|
|||||||
├── install.sh
|
├── install.sh
|
||||||
├── uninstall.sh
|
├── uninstall.sh
|
||||||
├── README.md
|
├── README.md
|
||||||
|
├── bin/
|
||||||
|
│ ├── sunshine-stream-do.sh # prep-cmd hook: create/resize headless on connect
|
||||||
|
│ └── sunshine-stream-undo.sh # prep-cmd hook: tear down headless on disconnect
|
||||||
|
├── client/
|
||||||
|
│ ├── install-macos.sh # Moonlight via brew cask
|
||||||
|
│ └── README.md # per-platform install + first-pair flow
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── common.sh # logging, sudo, idempotency helpers
|
│ ├── common.sh # logging, sudo, idempotency helpers
|
||||||
│ ├── detect.sh # GPU vendor, session type, hostname
|
│ ├── detect.sh # GPU vendor, session type, hostname
|
||||||
│ ├── preflight.sh # pre-install sanity checks (driver, modeset, audio, session)
|
│ ├── preflight.sh # pre-install sanity checks
|
||||||
│ ├── packages.sh # yay -S sunshine-bin moonlight-qt + GPU encoders
|
│ ├── packages.sh # yay -S sunshine-bin moonlight-qt + GPU encoders
|
||||||
│ ├── permissions.sh # input group, uinput udev, setcap cap_sys_admin
|
│ ├── permissions.sh # input group, uinput udev, setcap cap_sys_admin
|
||||||
│ ├── config.sh # writes tuned sunshine.conf (managed-by marker)
|
│ ├── config.sh # writes tuned sunshine.conf (managed-by marker)
|
||||||
│ ├── firewall.sh # ufw/firewalld detection + port opening
|
│ ├── headless.sh # installs prep-cmd hooks to ~/.local/share/
|
||||||
│ ├── service.sh # systemctl --user enable + loginctl enable-linger
|
│ ├── firewall.sh # ufw/firewalld detection + port opening
|
||||||
│ └── verify.sh # post-install checks (also reused by --doctor)
|
│ ├── service.sh # systemctl --user enable + enable-linger
|
||||||
└── files/ # (reserved — drop-in config files if needed later)
|
│ └── verify.sh # post-install checks (also reused by --doctor)
|
||||||
|
└── files/ # drop-in config files referenced by lib/
|
||||||
```
|
```
|
||||||
|
|||||||
66
bin/sunshine-stream-do.sh
Normal file
66
bin/sunshine-stream-do.sh
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Invoked by Sunshine as a stream-start hook (global_prep_cmd `do`).
|
||||||
|
# Creates/resizes a Hyprland headless output to match the connecting
|
||||||
|
# Moonlight client's resolution, and moves the active workspace onto it
|
||||||
|
# so the user's existing windows are visible on the stream.
|
||||||
|
#
|
||||||
|
# Sunshine env vars set on connect:
|
||||||
|
# SUNSHINE_CLIENT_WIDTH, SUNSHINE_CLIENT_HEIGHT, SUNSHINE_CLIENT_FPS
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf '[sunshine-do] %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
WIDTH="${SUNSHINE_CLIENT_WIDTH:-1920}"
|
||||||
|
HEIGHT="${SUNSHINE_CLIENT_HEIGHT:-1080}"
|
||||||
|
FPS="${SUNSHINE_CLIENT_FPS:-60}"
|
||||||
|
MON="HEADLESS-1"
|
||||||
|
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||||
|
log "hyprctl not found; cannot configure headless. Stream will use whatever output Sunshine selects."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Recover Hyprland signature if it wasn't inherited (defensive — UWSM exports it,
|
||||||
|
# but a stray service environment could miss it).
|
||||||
|
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
for sig_dir in "${XDG_RUNTIME_DIR:-/tmp}"/hypr/*/; do
|
||||||
|
[[ -d "$sig_dir" ]] || continue
|
||||||
|
export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig_dir")"
|
||||||
|
log "Discovered HYPRLAND_INSTANCE_SIGNATURE=$HYPRLAND_INSTANCE_SIGNATURE"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
log "Hyprland not running; nothing to configure."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Snapshot prior state so undo can restore.
|
||||||
|
hyprctl monitors -j > "$STATE_DIR/prev-monitors.json" 2>/dev/null || true
|
||||||
|
PREV_WS="$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // 1' || echo 1)"
|
||||||
|
echo "$PREV_WS" > "$STATE_DIR/prev-workspace-id"
|
||||||
|
|
||||||
|
# Ensure headless exists.
|
||||||
|
if ! hyprctl monitors all -j 2>/dev/null | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null; then
|
||||||
|
log "Creating headless output $MON"
|
||||||
|
hyprctl output create headless >/dev/null
|
||||||
|
# Brief settle so Hyprland registers the new output before we configure it.
|
||||||
|
for _ in 1 2 3 4 5; do
|
||||||
|
hyprctl monitors all -j | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null 2>&1 && break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resize headless to the client's resolution / framerate.
|
||||||
|
log "Sizing $MON → ${WIDTH}x${HEIGHT}@${FPS}"
|
||||||
|
hyprctl keyword monitor "$MON,${WIDTH}x${HEIGHT}@${FPS},auto,1" >/dev/null
|
||||||
|
|
||||||
|
# Move the active workspace onto the headless so existing windows appear in the stream.
|
||||||
|
log "Moving workspace $PREV_WS → $MON, focusing it"
|
||||||
|
hyprctl dispatch moveworkspacetomonitor "$PREV_WS $MON" >/dev/null || true
|
||||||
|
hyprctl dispatch focusmonitor "$MON" >/dev/null || true
|
||||||
|
|
||||||
|
log "Stream ready: ${WIDTH}x${HEIGHT}@${FPS} on $MON"
|
||||||
51
bin/sunshine-stream-undo.sh
Normal file
51
bin/sunshine-stream-undo.sh
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Invoked by Sunshine as a stream-stop hook (global_prep_cmd `undo`).
|
||||||
|
# Moves the previously-active workspace back to a real monitor (if any
|
||||||
|
# exist) and tears down the headless output created by sunshine-stream-do.sh.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf '[sunshine-undo] %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
MON="HEADLESS-1"
|
||||||
|
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/sunshine-headless"
|
||||||
|
|
||||||
|
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||||
|
log "hyprctl not found; nothing to undo."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
for sig_dir in "${XDG_RUNTIME_DIR:-/tmp}"/hypr/*/; do
|
||||||
|
[[ -d "$sig_dir" ]] || continue
|
||||||
|
export HYPRLAND_INSTANCE_SIGNATURE="$(basename "$sig_dir")"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
log "Hyprland not running; nothing to undo."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
PREV_WS="$(cat "$STATE_DIR/prev-workspace-id" 2>/dev/null || echo 1)"
|
||||||
|
|
||||||
|
# Find a non-headless monitor to move the workspace back to. If there isn't one
|
||||||
|
# (truly headless host with KVM detached), the workspace just lives on whatever
|
||||||
|
# Hyprland reassigns it to when we remove the output.
|
||||||
|
REAL_MON="$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.name | test("^HEADLESS") | not) | .name' | head -n1)"
|
||||||
|
if [[ -n "$REAL_MON" ]]; then
|
||||||
|
log "Returning workspace $PREV_WS → $REAL_MON"
|
||||||
|
hyprctl dispatch moveworkspacetomonitor "$PREV_WS $REAL_MON" >/dev/null || true
|
||||||
|
hyprctl dispatch focusmonitor "$REAL_MON" >/dev/null || true
|
||||||
|
else
|
||||||
|
log "No real monitor connected; leaving workspace assignment to Hyprland defaults."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if hyprctl monitors all -j 2>/dev/null | jq -e --arg m "$MON" '.[] | select(.name == $m)' >/dev/null; then
|
||||||
|
log "Removing $MON"
|
||||||
|
hyprctl output remove "$MON" >/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean state files but keep the directory for the next run.
|
||||||
|
rm -f "$STATE_DIR/prev-monitors.json" "$STATE_DIR/prev-workspace-id"
|
||||||
|
log "Stream teardown complete"
|
||||||
125
client/README.md
Normal file
125
client/README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Moonlight clients
|
||||||
|
|
||||||
|
This directory covers installing Moonlight on the devices that connect to your
|
||||||
|
Sunshine host. The host side (Sunshine on Linux) is documented in the
|
||||||
|
[top-level README](../README.md).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
From a Mac that has this repo checked out:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./client/install-macos.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script verifies Homebrew is present and runs `brew install --cask moonlight`.
|
||||||
|
It will not install Homebrew for you.
|
||||||
|
|
||||||
|
Manual equivalent:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install --cask moonlight
|
||||||
|
```
|
||||||
|
|
||||||
|
Fallback (no Homebrew): download a signed DMG from the official releases page
|
||||||
|
and drag `Moonlight.app` into `/Applications`:
|
||||||
|
|
||||||
|
- https://github.com/moonlight-stream/moonlight-qt/releases
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Install "Moonlight Game Streaming" from the Play Store:
|
||||||
|
|
||||||
|
- https://play.google.com/store/apps/details?id=com.limelight
|
||||||
|
|
||||||
|
### iOS / iPadOS
|
||||||
|
|
||||||
|
Install "Moonlight Game Streaming" from the App Store:
|
||||||
|
|
||||||
|
- https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566
|
||||||
|
|
||||||
|
### Apple TV
|
||||||
|
|
||||||
|
Same listing on the tvOS App Store. Search "Moonlight Game Streaming" on the
|
||||||
|
Apple TV itself, or use the App Store link above from an iOS device signed in
|
||||||
|
to the same Apple ID.
|
||||||
|
|
||||||
|
### Steam Deck / Linux
|
||||||
|
|
||||||
|
Use `moonlight-qt` from your distro's package manager (Flatpak on Steam Deck,
|
||||||
|
`pacman`/`apt` elsewhere). It is the same Qt-based client as macOS and Windows.
|
||||||
|
|
||||||
|
## First pair
|
||||||
|
|
||||||
|
The pairing flow is the same on every client.
|
||||||
|
|
||||||
|
1. Confirm the host is healthy. On the host machine:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./install.sh --doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
Sunshine must be running and reachable on the LAN.
|
||||||
|
|
||||||
|
2. On the client, open Moonlight. The host should appear automatically via
|
||||||
|
mDNS as long as the client is on the same LAN. If it does not appear, use
|
||||||
|
"Add Host Manually" and enter the host's LAN IP address.
|
||||||
|
|
||||||
|
3. Click or tap the host. Moonlight displays a 4-digit PIN.
|
||||||
|
|
||||||
|
4. On the host machine, open the Sunshine web UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://localhost:47990
|
||||||
|
```
|
||||||
|
|
||||||
|
Accept the self-signed certificate. Log in (credentials are set the first
|
||||||
|
time you visit). Go to the PIN tab, enter the 4-digit PIN from the client,
|
||||||
|
and submit. Pairing typically completes within about 5 seconds.
|
||||||
|
|
||||||
|
5. Back in Moonlight, the host now shows as "Paired". Click it to see the
|
||||||
|
available apps. The default app is "Desktop", which is either a full-screen
|
||||||
|
mirror of the host's display or a headless virtual display, depending on
|
||||||
|
how the host is configured.
|
||||||
|
|
||||||
|
## Streaming notes
|
||||||
|
|
||||||
|
- **Resolution.** Moonlight lets you pick a target stream resolution per host.
|
||||||
|
On a headless-configured host, that resolution is what the host actually
|
||||||
|
renders at. On a mirror-configured host, the host renders at its native
|
||||||
|
resolution and Moonlight downscales on the client.
|
||||||
|
|
||||||
|
- **Bitrate.** Moonlight's defaults are conservative for general internet use.
|
||||||
|
For LAN streaming, bump it to 50-100 Mbps.
|
||||||
|
|
||||||
|
- **Audio.** By default, Moonlight captures audio from the host and plays it
|
||||||
|
on the client. If you want the host's speakers/monitor to keep playing
|
||||||
|
audio while you stream, enable "Play audio on host" in Moonlight's stream
|
||||||
|
settings.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Host not found" / host does not appear automatically.** Confirm both
|
||||||
|
machines are on the same LAN/VLAN. mDNS does not traverse VLANs without an
|
||||||
|
mDNS reflector. Fall back to "Add Host Manually" with the host's IP.
|
||||||
|
|
||||||
|
- **Pairing PIN does not work.** Two common causes: the Sunshine web UI
|
||||||
|
session has timed out (log in again and re-enter the PIN), or the PIN is
|
||||||
|
being entered into the wrong host (multiple Sunshine instances on the LAN).
|
||||||
|
Cancel and re-trigger pair from the client to get a fresh PIN.
|
||||||
|
|
||||||
|
- **Black screen after pairing.** This is a host-side problem. On the host:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./install.sh --doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Mac cannot reach `brew`.** Homebrew is not installed (or not on `PATH`).
|
||||||
|
Install it per https://brew.sh, open a fresh shell, then re-run
|
||||||
|
`./client/install-macos.sh`.
|
||||||
|
|
||||||
|
- **Android sees the host but cannot connect.** The Android device is
|
||||||
|
probably on a guest WiFi SSID that is isolated from the main LAN. Move it
|
||||||
|
to the main LAN, or use "Add Host Manually" with the host IP.
|
||||||
61
client/install-macos.sh
Executable file
61
client/install-macos.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install Moonlight on macOS via Homebrew cask.
|
||||||
|
# Standalone: does not source lib/common.sh (intended to run on a Mac that
|
||||||
|
# may not have the full repo checked out yet).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
BOLD=$'\033[1m'
|
||||||
|
RED=$'\033[31m'
|
||||||
|
GREEN=$'\033[32m'
|
||||||
|
YELLOW=$'\033[33m'
|
||||||
|
BLUE=$'\033[34m'
|
||||||
|
RESET=$'\033[0m'
|
||||||
|
else
|
||||||
|
BOLD="" RED="" GREEN="" YELLOW="" BLUE="" RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
step() { printf '\n%s==>%s %s%s%s\n' "$BLUE" "$RESET" "$BOLD" "$*" "$RESET"; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
ok() { printf ' %s✓%s %s\n' "$GREEN" "$RESET" "$*"; }
|
||||||
|
warn() { printf ' %s!%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
|
||||||
|
err() { printf ' %s✗%s %s\n' "$RED" "$RESET" "$*" >&2; }
|
||||||
|
|
||||||
|
# Refuse to run anywhere but macOS.
|
||||||
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||||
|
err "This script only runs on macOS (Darwin). Detected: $(uname -s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Checking for Homebrew"
|
||||||
|
if ! command -v brew >/dev/null 2>&1; then
|
||||||
|
err "Homebrew is not installed."
|
||||||
|
info "Install it with the official one-liner, then re-run this script:"
|
||||||
|
info ""
|
||||||
|
info ' /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
|
||||||
|
info ""
|
||||||
|
info "See https://brew.sh for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Homebrew found: $(command -v brew)"
|
||||||
|
|
||||||
|
step "Installing Moonlight (brew cask)"
|
||||||
|
# brew install --cask is idempotent: re-running on an already-installed cask
|
||||||
|
# is a no-op and exits 0.
|
||||||
|
if brew install --cask moonlight; then
|
||||||
|
ok "Moonlight installed (or already present)."
|
||||||
|
else
|
||||||
|
err "brew install --cask moonlight failed."
|
||||||
|
warn "Try: brew update && brew install --cask moonlight"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Next steps"
|
||||||
|
info "App location: /Applications/Moonlight.app"
|
||||||
|
info "Launch: open -a Moonlight"
|
||||||
|
info ""
|
||||||
|
info "Pair this Mac with your Sunshine host by following the walkthrough in:"
|
||||||
|
info " client/README.md"
|
||||||
|
info ""
|
||||||
|
ok "Done."
|
||||||
32
install.sh
32
install.sh
@@ -24,6 +24,8 @@ source "$SCRIPT_DIR/lib/firewall.sh"
|
|||||||
source "$SCRIPT_DIR/lib/service.sh"
|
source "$SCRIPT_DIR/lib/service.sh"
|
||||||
# shellcheck source=lib/verify.sh
|
# shellcheck source=lib/verify.sh
|
||||||
source "$SCRIPT_DIR/lib/verify.sh"
|
source "$SCRIPT_DIR/lib/verify.sh"
|
||||||
|
# shellcheck source=lib/headless.sh
|
||||||
|
source "$SCRIPT_DIR/lib/headless.sh"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -38,6 +40,8 @@ Options:
|
|||||||
--no-sunshine Don't install sunshine (client-only setup)
|
--no-sunshine Don't install sunshine (client-only setup)
|
||||||
--no-config Don't write a tuned sunshine.conf
|
--no-config Don't write a tuned sunshine.conf
|
||||||
--from-source Build Sunshine from source (equivalent to SUNSHINE_PKG=sunshine)
|
--from-source Build Sunshine from source (equivalent to SUNSHINE_PKG=sunshine)
|
||||||
|
--headless Force headless streaming mode (wlr capture of HEADLESS-1)
|
||||||
|
--mirror Force mirror mode (KMS capture of the real display)
|
||||||
--doctor Run only the post-install verification checks
|
--doctor Run only the post-install verification checks
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ INSTALL_SUNSHINE=1
|
|||||||
INSTALL_MOONLIGHT=1
|
INSTALL_MOONLIGHT=1
|
||||||
WRITE_CONFIG=1
|
WRITE_CONFIG=1
|
||||||
DOCTOR_ONLY=0
|
DOCTOR_ONLY=0
|
||||||
|
MODE_OVERRIDE=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -62,6 +67,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--no-sunshine) INSTALL_SUNSHINE=0 ;;
|
--no-sunshine) INSTALL_SUNSHINE=0 ;;
|
||||||
--no-config) WRITE_CONFIG=0 ;;
|
--no-config) WRITE_CONFIG=0 ;;
|
||||||
--from-source) export SUNSHINE_PKG=sunshine ;;
|
--from-source) export SUNSHINE_PKG=sunshine ;;
|
||||||
|
--headless) MODE_OVERRIDE="headless" ;;
|
||||||
|
--mirror) MODE_OVERRIDE="mirror" ;;
|
||||||
--doctor) DOCTOR_ONLY=1 ;;
|
--doctor) DOCTOR_ONLY=1 ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) err "Unknown option: $1"; usage; exit 2 ;;
|
*) err "Unknown option: $1"; usage; exit 2 ;;
|
||||||
@@ -69,6 +76,21 @@ while [[ $# -gt 0 ]]; do
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Pick streaming mode: explicit flag wins; otherwise default to headless on
|
||||||
|
# JARVIS (the KVM-attached primary target) and mirror everywhere else.
|
||||||
|
compute_stream_mode() {
|
||||||
|
if [[ -n "$MODE_OVERRIDE" ]]; then
|
||||||
|
STREAM_MODE="$MODE_OVERRIDE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local host_lc="${HOSTNAME_SHORT,,}"
|
||||||
|
if [[ "$host_lc" == "jarvis" ]]; then
|
||||||
|
STREAM_MODE="headless"
|
||||||
|
else
|
||||||
|
STREAM_MODE="mirror"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
require_not_root
|
require_not_root
|
||||||
require_arch
|
require_arch
|
||||||
@@ -76,7 +98,10 @@ main() {
|
|||||||
|
|
||||||
step "Detecting system"
|
step "Detecting system"
|
||||||
detect_all
|
detect_all
|
||||||
|
compute_stream_mode
|
||||||
|
export STREAM_MODE
|
||||||
info "Host: $HOSTNAME_SHORT GPU: $GPU_VENDOR Session: $SESSION_TYPE"
|
info "Host: $HOSTNAME_SHORT GPU: $GPU_VENDOR Session: $SESSION_TYPE"
|
||||||
|
info "Mode: $STREAM_MODE"
|
||||||
|
|
||||||
if [[ $DOCTOR_ONLY -eq 1 ]]; then
|
if [[ $DOCTOR_ONLY -eq 1 ]]; then
|
||||||
verify_install
|
verify_install
|
||||||
@@ -96,9 +121,14 @@ main() {
|
|||||||
ensure_uinput_udev_rule
|
ensure_uinput_udev_rule
|
||||||
set_sunshine_capabilities
|
set_sunshine_capabilities
|
||||||
|
|
||||||
|
if [[ "$STREAM_MODE" == "headless" ]]; then
|
||||||
|
step "Installing headless prep-cmd hooks"
|
||||||
|
install_headless_hooks
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $WRITE_CONFIG -eq 1 ]]; then
|
if [[ $WRITE_CONFIG -eq 1 ]]; then
|
||||||
step "Writing tuned sunshine.conf"
|
step "Writing tuned sunshine.conf"
|
||||||
write_sunshine_config
|
write_sunshine_config "$STREAM_MODE"
|
||||||
else
|
else
|
||||||
info "Skipping sunshine.conf (--no-config)"
|
info "Skipping sunshine.conf (--no-config)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ SUNSHINE_CONF="$SUNSHINE_CONF_DIR/sunshine.conf"
|
|||||||
MANAGED_MARKER="# managed-by: omarchy-moonlight"
|
MANAGED_MARKER="# managed-by: omarchy-moonlight"
|
||||||
|
|
||||||
write_sunshine_config() {
|
write_sunshine_config() {
|
||||||
|
local mode="${1:-mirror}"
|
||||||
mkdir -p "$SUNSHINE_CONF_DIR"
|
mkdir -p "$SUNSHINE_CONF_DIR"
|
||||||
|
|
||||||
if [[ -f "$SUNSHINE_CONF" ]] && ! grep -qF "$MANAGED_MARKER" "$SUNSHINE_CONF"; then
|
if [[ -f "$SUNSHINE_CONF" ]] && ! grep -qF "$MANAGED_MARKER" "$SUNSHINE_CONF"; then
|
||||||
@@ -31,19 +32,36 @@ write_sunshine_config() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
info "Writing tuned $SUNSHINE_CONF (GPU: $GPU_VENDOR)"
|
local capture_block
|
||||||
|
case "$mode" in
|
||||||
|
headless)
|
||||||
|
# wlr capture of the Hyprland headless output that prep-cmd hooks create.
|
||||||
|
# global_prep_cmd is parsed as a JSON array by Sunshine, so keep it on one line.
|
||||||
|
capture_block="# Capture: wlr — captures a Hyprland headless output created on stream start.
|
||||||
|
capture = wlr
|
||||||
|
output_name = HEADLESS-1
|
||||||
|
|
||||||
|
# Hooks that create/resize HEADLESS-1 on stream start and remove it on stop.
|
||||||
|
global_prep_cmd = [{\"do\":\"${DO_SCRIPT}\",\"undo\":\"${UNDO_SCRIPT}\"}]"
|
||||||
|
;;
|
||||||
|
mirror|*)
|
||||||
|
capture_block="# Capture: KMS (DRM) — the right choice on Wayland.
|
||||||
|
capture = kms"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
info "Writing tuned $SUNSHINE_CONF (GPU: $GPU_VENDOR, mode: $mode)"
|
||||||
cat >"$SUNSHINE_CONF" <<EOF
|
cat >"$SUNSHINE_CONF" <<EOF
|
||||||
$MANAGED_MARKER
|
$MANAGED_MARKER
|
||||||
# Generated by omarchy-moonlight install.sh on $(date -Iseconds)
|
# Generated by omarchy-moonlight install.sh on $(date -Iseconds)
|
||||||
# Delete this marker line to take ownership; the installer will then leave the file alone.
|
# Delete this marker line to take ownership; the installer will then leave the file alone.
|
||||||
#
|
#
|
||||||
# Tuned for low-latency LAN streaming on Hyprland/Wayland with KMS capture.
|
# Tuned for low-latency LAN streaming on Hyprland/Wayland.
|
||||||
# Tune further via the web UI at https://localhost:47990
|
# Tune further via the web UI at https://localhost:47990
|
||||||
|
|
||||||
sunshine_name = ${HOSTNAME_SHORT}
|
sunshine_name = ${HOSTNAME_SHORT}
|
||||||
|
|
||||||
# Capture: KMS (DRM) — the right choice on Wayland.
|
$capture_block
|
||||||
capture = kms
|
|
||||||
|
|
||||||
# Encoder tuned for $GPU_VENDOR
|
# Encoder tuned for $GPU_VENDOR
|
||||||
$encoder_block
|
$encoder_block
|
||||||
|
|||||||
20
lib/headless.sh
Normal file
20
lib/headless.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set up headless streaming mode: install prep-cmd hook scripts into a stable
|
||||||
|
# location and ensure they're executable. The actual sunshine.conf entries
|
||||||
|
# (capture=wlr, output_name=HEADLESS-1, global_prep_cmd=[...]) are written
|
||||||
|
# by lib/config.sh.
|
||||||
|
|
||||||
|
HEADLESS_BIN_DIR="$HOME/.local/share/omarchy-moonlight/bin"
|
||||||
|
|
||||||
|
DO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-do.sh"
|
||||||
|
UNDO_SCRIPT="$HEADLESS_BIN_DIR/sunshine-stream-undo.sh"
|
||||||
|
export DO_SCRIPT UNDO_SCRIPT
|
||||||
|
|
||||||
|
install_headless_hooks() {
|
||||||
|
# Install hook scripts to ~/.local/share so they don't disappear if the
|
||||||
|
# repo gets moved or deleted. Sunshine's config will reference these stable paths.
|
||||||
|
mkdir -p "$HEADLESS_BIN_DIR"
|
||||||
|
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-do.sh" "$DO_SCRIPT"
|
||||||
|
install -m 0755 "$SCRIPT_DIR/bin/sunshine-stream-undo.sh" "$UNDO_SCRIPT"
|
||||||
|
ok "Installed prep-cmd hooks to $HEADLESS_BIN_DIR"
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
install_sunshine() {
|
install_sunshine() {
|
||||||
# Ensure runtime deps useful for capture/diagnostics across vendors.
|
# Ensure runtime deps useful for capture/diagnostics across vendors.
|
||||||
yay_install pipewire-pulse vulkan-tools libva-utils
|
yay_install pipewire-pulse vulkan-tools libva-utils jq
|
||||||
yay_install "$SUNSHINE_PKG"
|
yay_install "$SUNSHINE_PKG"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ preflight_all() {
|
|||||||
preflight_gpu
|
preflight_gpu
|
||||||
preflight_kms_modeset
|
preflight_kms_modeset
|
||||||
preflight_audio
|
preflight_audio
|
||||||
|
preflight_headless
|
||||||
}
|
}
|
||||||
|
|
||||||
preflight_session() {
|
preflight_session() {
|
||||||
@@ -96,3 +97,33 @@ preflight_audio() {
|
|||||||
warn "pipewire-pulse is not installed. Audio capture may not work until it is."
|
warn "pipewire-pulse is not installed. Audio capture may not work until it is."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preflight_headless() {
|
||||||
|
# Only relevant in headless mode. Checks are non-fatal: install can proceed
|
||||||
|
# even if Hyprland isn't reachable right now (hooks just won't function until
|
||||||
|
# the user logs into Hyprland on the host).
|
||||||
|
[[ "${STREAM_MODE:-}" == "headless" ]] || return 0
|
||||||
|
|
||||||
|
if command -v hyprctl >/dev/null 2>&1; then
|
||||||
|
ok "hyprctl on PATH"
|
||||||
|
else
|
||||||
|
warn "hyprctl not found. Headless prep-cmd hooks will fail until Hyprland is installed and reachable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pkg_installed jq; then
|
||||||
|
ok "jq installed (prep-cmd hooks have their parser)"
|
||||||
|
else
|
||||||
|
info "jq not installed yet — will be installed in the packages step."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
|
||||||
|
ok "Hyprland instance signature present in environment"
|
||||||
|
else
|
||||||
|
local rt="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||||
|
if compgen -G "$rt/hypr/*/" >/dev/null 2>&1; then
|
||||||
|
ok "Hyprland runtime directory found under $rt/hypr/"
|
||||||
|
else
|
||||||
|
warn "Hyprland does not appear to be running. Install will proceed; hooks will only work once you log into Hyprland on the host."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,6 +67,40 @@ verify_install() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if [[ "${STREAM_MODE:-}" == "headless" ]]; then
|
||||||
|
local do_script="$HOME/.local/share/omarchy-moonlight/bin/sunshine-stream-do.sh"
|
||||||
|
local undo_script="$HOME/.local/share/omarchy-moonlight/bin/sunshine-stream-undo.sh"
|
||||||
|
local conf="$HOME/.config/sunshine/sunshine.conf"
|
||||||
|
|
||||||
|
if [[ -x "$do_script" ]]; then
|
||||||
|
ok "headless do-hook present and executable"
|
||||||
|
else
|
||||||
|
err "headless do-hook missing or not executable: $do_script"
|
||||||
|
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x "$undo_script" ]]; then
|
||||||
|
ok "headless undo-hook present and executable"
|
||||||
|
else
|
||||||
|
err "headless undo-hook missing or not executable: $undo_script"
|
||||||
|
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$conf" ]] && grep -q '^capture = wlr' "$conf"; then
|
||||||
|
ok "sunshine.conf has capture = wlr"
|
||||||
|
else
|
||||||
|
err "sunshine.conf missing 'capture = wlr'"
|
||||||
|
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$conf" ]] && grep -q '^global_prep_cmd' "$conf"; then
|
||||||
|
ok "sunshine.conf has global_prep_cmd"
|
||||||
|
else
|
||||||
|
err "sunshine.conf missing 'global_prep_cmd'"
|
||||||
|
VERIFY_FAILURES=$((VERIFY_FAILURES + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if systemctl --user is-active --quiet sunshine.service; then
|
if systemctl --user is-active --quiet sunshine.service; then
|
||||||
ok "sunshine.service is active"
|
ok "sunshine.service is active"
|
||||||
if ss -ltn 2>/dev/null | grep -q ':47990 '; then
|
if ss -ltn 2>/dev/null | grep -q ':47990 '; then
|
||||||
|
|||||||
Reference in New Issue
Block a user