Files
ubuntu-post-install/services/sunshine.sh
T
Claude db9fafb626 Fix standalone bootstrap: move to after function definition
When run directly (sudo bash services/foo.sh), bash executes the bootstrap
block before reaching the install_* function definition below it, causing
"command not found". Moving the bootstrap to the end of each file ensures
the function is defined by the time the bootstrap calls it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
2026-06-26 00:53:55 +00:00

393 lines
16 KiB
Bash

#!/bin/bash
# services/sunshine.sh — Sunshine game streaming host (Moonlight-compatible).
# Part of the modular post-install system (sourced by setup.sh).
#
# Sunshine streams your desktop or a specific app to any Moonlight client
# (phone, TV, another PC, etc.) with hardware-accelerated encoding and low
# latency. Ideal for playing games on a GPU machine from a thin client.
#
# ── Coexistence with Wolf ──────────────────────────────────────────────────
# Wolf and Sunshine both use Moonlight protocol ports. This installer offsets
# Sunshine to port base 48090 (vs Wolf's default 47984) so both can run
# simultaneously. On Moonlight clients, add the host twice — once on default
# port (Wolf) and once on port 48090 (Sunshine).
#
# To switch between them if you prefer not to run both:
# sudo systemctl stop wolf && sudo systemctl start sunshine
# sudo systemctl stop sunshine && sudo systemctl start wolf
#
# ── Closed-lid laptop / headless ──────────────────────────────────────────
# Sunshine captures a real display (Xorg/Wayland). For closed-lid use:
# Option A: Dummy HDMI/DisplayPort plug (~$5) — simplest, most reliable
# Option B: Virtual display via xrandr (software only, set up below)
#
# ── Controller input ──────────────────────────────────────────────────────
# Moonlight sends controller input to Sunshine via uinput (virtual gamepad).
# Works for most games; requires uinput udev rules (applied by this script).
# Keyboard and mouse work reliably for all games including SWBF2 2017.
#
# ── Kyber / SWBF2 app entry ───────────────────────────────────────────────
# This script pre-configures a "Kyber SWBF2" app entry in Sunshine so
# Moonlight shows it as a launchable app. The Kyber AppImage must already
# be installed (run setup-kyber-linux.sh first).
# ── Registration ──────────────────────────────────────────────────────────
register_service sunshine homelab "Sunshine game streaming host for Moonlight clients (coexists with Wolf on offset ports)"
# ── Install function ──────────────────────────────────────────────────────
install_sunshine() {
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Sunshine — Game Streaming Host ║"
echo "║ Stream to Moonlight on any device ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] Would download and install Sunshine .deb"
echo "[DRY-RUN] Would configure uinput udev rules"
echo "[DRY-RUN] Would write Sunshine config (offset ports for Wolf coexistence)"
echo "[DRY-RUN] Would add Kyber SWBF2 app entry if Kyber AppImage found"
echo "[DRY-RUN] Would optionally set up virtual display for closed-lid use"
echo "[DRY-RUN] Would enable and start sunshine systemd service"
return 0
fi
# ── Detect Ubuntu version for correct .deb ────────────────────────────
local UBUNTU_VER
UBUNTU_VER=$(lsb_release -rs 2>/dev/null || echo "24.04")
local DEB_DISTRO
case "$UBUNTU_VER" in
22*) DEB_DISTRO="ubuntu-22.04" ;;
24*) DEB_DISTRO="ubuntu-24.04" ;;
26*) DEB_DISTRO="ubuntu-26.04" ;;
*) DEB_DISTRO="ubuntu-24.04" ;;
esac
# ── Download and install Sunshine ─────────────────────────────────────
if command -v sunshine &>/dev/null; then
log_info "Sunshine already installed: $(sunshine --version 2>/dev/null || echo 'version unknown')"
else
log_info "Fetching latest Sunshine release for $DEB_DISTRO..."
local API_URL="https://api.github.com/repos/LizardByte/Sunshine/releases/latest"
local DEB_URL
DEB_URL=$(curl -fsSL "$API_URL" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for a in data.get('assets', []):
name = a['name']
if '${DEB_DISTRO}' in name and name.endswith('.deb') and 'debuginfo' not in name:
print(a['browser_download_url'])
break
" 2>/dev/null)
if [[ -z "$DEB_URL" ]]; then
log_error "Could not find Sunshine .deb for $DEB_DISTRO."
log_error "Download manually from: https://github.com/LizardByte/Sunshine/releases/latest"
return 1
fi
local DEB_FILE="/tmp/sunshine.deb"
log_info "Downloading: $DEB_URL"
curl -L --progress-bar -o "$DEB_FILE" "$DEB_URL"
apt-get install -y "$DEB_FILE" || {
log_error "Sunshine install failed."
return 1
}
rm -f "$DEB_FILE"
log_success "Sunshine installed."
fi
# ── uinput udev rules (controller/mouse input from Moonlight) ─────────
log_info "Configuring uinput for controller and mouse input..."
cat > /etc/udev/rules.d/85-sunshine-input.rules << 'EOF'
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
EOF
usermod -aG input "$ACTUAL_USER" 2>/dev/null || true
udevadm control --reload-rules && udevadm trigger || true
log_success "uinput configured (user added to 'input' group)."
# ── Sunshine config directory ─────────────────────────────────────────
local CONF_DIR="$ACTUAL_HOME/.config/sunshine"
mkdir -p "$CONF_DIR"
# ── Port config (offset from Wolf to allow coexistence) ───────────────
log_info "Configuring Sunshine ports (offset from Wolf defaults)..."
cat > "$CONF_DIR/sunshine.conf" << 'EOF'
# Sunshine configuration
# Ports offset from Wolf defaults so both can run simultaneously.
# Wolf uses: 47984/47989/48010
# Sunshine uses: 48090/48095/48100
# On Moonlight clients, add this host manually with port 48090.
port = 48090
# Hardware encoding — auto-detects GPU (NVIDIA NVENC, Intel QSV, AMD VCE)
encoder = auto
# Logging
log_path = /tmp/sunshine.log
min_log_level = info
EOF
# ── Kyber app entry ───────────────────────────────────────────────────
local KYBER_PATH=""
for p in \
"$ACTUAL_HOME/Applications/KyberLinuxPort-x86_64.AppImage" \
"$ACTUAL_HOME/.local/share/kyber/KyberLinuxPort.AppImage" \
"$ACTUAL_HOME/Downloads/kyber/KyberLinuxPort.AppImage"
do
if [[ -f "$p" ]]; then
KYBER_PATH="$p"
break
fi
done
local APPS_JSON="$CONF_DIR/apps.json"
if [[ -n "$KYBER_PATH" ]]; then
log_info "Kyber AppImage found at $KYBER_PATH — adding app entry..."
cat > "$APPS_JSON" << EOF
{
"env": {},
"apps": [
{
"name": "Kyber SWBF2",
"output": "",
"cmd": "${KYBER_PATH}",
"exclude-global-prep-cmd": false,
"elevated": false,
"auto-detach": true,
"wait-all": true,
"exit-timeout": 5,
"image-path": ""
},
{
"name": "Desktop",
"output": "",
"cmd": "",
"exclude-global-prep-cmd": false,
"elevated": false,
"auto-detach": true,
"wait-all": true,
"exit-timeout": 5,
"image-path": ""
}
]
}
EOF
log_success "Kyber SWBF2 app entry added."
else
log_warning "Kyber AppImage not found — skipping Kyber app entry."
log_warning "Run setup-kyber-linux.sh first, then re-run this installer."
cat > "$APPS_JSON" << 'EOF'
{
"env": {},
"apps": [
{
"name": "Desktop",
"output": "",
"cmd": "",
"exclude-global-prep-cmd": false,
"elevated": false,
"auto-detach": true,
"wait-all": true,
"exit-timeout": 5,
"image-path": ""
}
]
}
EOF
fi
ensure_docker_dir_ownership "$CONF_DIR"
# ── Virtual display for closed-lid / headless ─────────────────────────
local VIRTUAL_DISPLAY=""
prompt_yn "Set up virtual display for closed-lid/headless use? (y/n):" "n" VIRTUAL_DISPLAY
if [[ "$VIRTUAL_DISPLAY" =~ ^[Yy]$ ]]; then
log_info "Installing virtual display support..."
apt-get install -y xserver-xorg-video-dummy x11-xserver-utils 2>/dev/null || true
cat > /etc/X11/xorg.conf.d/20-sunshine-virtual.conf << 'EOF'
# Virtual display for Sunshine streaming (closed-lid / headless)
# Provides a 1920x1080 virtual monitor so Sunshine always has a display to capture.
Section "Device"
Identifier "DummyDevice"
Driver "dummy"
VideoRam 256000
EndSection
Section "Screen"
Identifier "DummyScreen"
Device "DummyDevice"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1920x1080"
EndSubSection
EndSection
Section "Monitor"
Identifier "DummyMonitor"
HorizSync 28.0-80.0
VertRefresh 48.0-75.0
Modeline "1920x1080" 148.50 1920 2008 2052 2200 1080 1084 1089 1125 +hsync +vsync
EndSection
EOF
log_success "Virtual display configured at 1920x1080."
log_warning "A physical dummy HDMI plug is simpler and more reliable."
log_warning "If you have one, plug it in and skip this virtual display config."
fi
# ── Systemd service ───────────────────────────────────────────────────
log_info "Enabling Sunshine systemd service..."
# Sunshine installs its own systemd user service
sudo -u "$ACTUAL_USER" systemctl --user enable sunshine 2>/dev/null || true
# ── Wolf coexistence note ─────────────────────────────────────────────
if systemctl is-enabled wolf &>/dev/null 2>&1; then
log_warning "Wolf is installed on this machine."
log_warning "Both can run simultaneously — Sunshine uses ports 48090-48100."
log_warning "On Moonlight, add this host twice:"
log_warning " Wolf: $(hostname -I | awk '{print $1}') (default port)"
log_warning " Sunshine: $(hostname -I | awk '{print $1}'):48090"
fi
# ── README ────────────────────────────────────────────────────────────
local DIR="$DOCKER_DIR/sunshine"
mkdir -p "$DIR"
ensure_docker_dir_ownership "$DIR"
write_readme "$DIR" << 'MD'
# Sunshine — Game Streaming Host
Streams your desktop or apps to any Moonlight client with hardware-accelerated
encoding (NVIDIA NVENC / Intel QSV / AMD VCE).
## Manage
```bash
systemctl --user start sunshine # start
systemctl --user stop sunshine # stop
systemctl --user restart sunshine # restart
systemctl --user status sunshine # check status
```
## Web UI
Configure apps and settings at: https://localhost:47990
(First run prompts you to create a username and password)
## Moonlight client setup
1. Install Moonlight on the client machine:
- Ubuntu/Debian: sudo apt install moonlight-qt
- Or AppImage from https://moonlight-stream.org
2. On first connection, Moonlight shows a PIN — enter it in the Sunshine web UI
3. Sunshine is on port 48090 (offset from Wolf). Add the host manually:
- In Moonlight: Add Host → enter IP:48090
## Coexistence with Wolf
Sunshine (ports 48090-48100) and Wolf (ports 47984-47989) can run simultaneously.
On Moonlight, add two separate host entries — one for each.
To run only one at a time:
```bash
# Switch to Sunshine
sudo systemctl stop wolf && systemctl --user start sunshine
# Switch back to Wolf
systemctl --user stop sunshine && sudo systemctl start wolf
```
## Closed-lid / headless use
A cheap HDMI/DisplayPort dummy plug is the most reliable solution.
Plugin it into the GPU output and close the lid — Sunshine streams the
virtual monitor the dummy plug creates.
Alternative: virtual display via xrandr (if xserver-xorg-video-dummy installed):
```bash
xrandr --addmode VIRTUAL1 1920x1080
xrandr --output VIRTUAL1 --mode 1920x1080
```
## Kyber SWBF2
The "Kyber SWBF2" app appears in Moonlight if the Kyber AppImage was found
at install time. Click it in Moonlight to launch Kyber on the host machine.
If not shown, re-run: sudo ./setup.sh sunshine
## Keyboard and mouse
Moonlight passes keyboard and mouse input transparently — works perfectly
for all games including SWBF2 2017.
## Controller input
Moonlight injects a virtual gamepad via uinput. Works for most games.
SWBF2 2017 is fully playable with keyboard and mouse if controller has issues.
MD
# ── Start now? ────────────────────────────────────────────────────────
local START=""
prompt_yn "Start Sunshine now? (y/n):" "y" START
if [[ "$START" =~ ^[Yy]$ ]]; then
sudo -u "$ACTUAL_USER" systemctl --user start sunshine \
&& log_success "Sunshine started." \
|| log_warning "Start failed — check: systemctl --user status sunshine"
fi
echo ""
log_success "Sunshine installed."
echo ""
echo " Web UI: https://localhost:47990 (set username/password on first visit)"
echo " Moonlight: add this host at $(hostname -I | awk '{print $1}'):48090"
echo " Logs: /tmp/sunshine.log"
if [[ -n "$KYBER_PATH" ]]; then
echo " Kyber app: 'Kyber SWBF2' visible in Moonlight app list"
fi
echo ""
echo "NOTE: Log out and back in (or reboot) for the 'input' group to take effect"
echo " (required for controller and mouse input from Moonlight)."
}
# ── Standalone bootstrap ──────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_COMMON="$_SELF_DIR/../lib/common.sh"
if [[ -f "$_COMMON" ]]; then
source "$_COMMON"
else
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
require_docker() { return 0; }
ensure_docker_dir_ownership() {
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
}
prompt_text() {
local _q="$1" _def="$2" _var="$3" _r
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
read -r -p " $_q " _r
eval "$_var='${_r:-$_def}'"
}
prompt_yn() {
local _q="$1" _def="$2" _var="$3" _r
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
read -r -p " $_q " _r
eval "$_var='${_r:-$_def}'"
}
ACTUAL_USER="${SUDO_USER:-$USER}"
ACTUAL_HOME=$(eval echo "~$ACTUAL_USER")
DOCKER_DIR="$ACTUAL_HOME/docker"
DRY_RUN="${DRY_RUN:-false}"
UNATTENDED="${UNATTENDED:-false}"
fi
install_sunshine
exit $?
fi