1 Commits
Author SHA1 Message Date
Andre 995cf19fc9 feat: configure Sunshine display selection
Coarse DisplaySwitch projection modes cannot reliably target the virtual display once multiple physical monitors are connected. The setup needs to persist Sunshine's device-specific configuration and document the recovery path so stream startup and abnormal shutdown handling remain predictable.
2026-06-09 20:45:40 +02:00
4 changed files with 709 additions and 56 deletions
+86 -50
View File
@@ -6,104 +6,140 @@
## English ## English
Solves a problem with game streaming via **Sunshine + Moonlight**: The physical PC monitor is disabled during the stream and reliably reactivated afterwards. Configures **Sunshine + Moonlight** to stream from a selected display device. This is intended for setups with physical monitors plus an IDD virtual monitor: Sunshine activates the selected monitor for the stream and deactivates the other monitors while the stream is active.
### Files ### Files
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `idd_an.bat` | Sunshine Do-Command — enables IDD virtual monitor, disables physical monitor | | `select_sunshine_monitor.bat` | All-in-one terminal tool to list monitors, select the streaming monitor, write Sunshine settings, and reset them |
| `idd_aus.bat` | Sunshine Undo-Command — reactivates physical monitor | | `register_task.bat` | Optional logon fallback for abnormal shutdown while the stream is still using the IDD-only topology |
| `register_task.bat` | Run once — registers a logon task as fallback if the PC is shut down during a stream |
### Setup ### Setup
#### 1. Configure Sunshine Run `select_sunshine_monitor.bat` by double-clicking it.
In the Sunshine web UI under **Configuration > General**: The terminal menu shows all Sunshine display devices with their IDs and status:
| Field | Value | - enter a number to select the monitor Sunshine should use for streaming
|---|---| - enter `R` to remove the display-device settings and return Sunshine to its defaults
| Command Preparations — Do | `C:\Projekte\SunshineDisplaySwitch\idd_an.bat` | - enter `Q` to cancel
| Command Preparations — Undo | `C:\Projekte\SunshineDisplaySwitch\idd_aus.bat` |
#### 2. Register fallback task (once) Select the IDD virtual monitor, for example `IDD HDR`. If Windows asks for administrator permission, allow it so the tool can update Sunshine's config under `C:\Program Files\Sunshine\config\sunshine.conf`.
Run `register_task.bat` as administrator. Registers `DisplaySwitch /internal` as a logon task (`MoonlightTeardown`). This activates if the PC is shut down during a stream and the physical monitor is still disabled on next boot. After saving, restart Sunshine when prompted. Stop the current Moonlight session completely and start a new stream.
### How it works ### Optional Startup Fallback
``` Sunshine restores the original display topology after a normal stream end. If Windows is shut down from Steam or another streamed app while the stream is still active, Sunshine may not get a chance to revert the IDD-only topology. In that case Windows can boot with the physical monitor still disabled.
Stream starts
└─> idd_an.bat
└─> DisplaySwitch.exe /external (IDD active, physical monitor off)
Stream ends Run `register_task.bat` as administrator if you want a logon fallback for that case. It registers the `MoonlightTeardown` task for the current user and runs `DisplaySwitch.exe /internal` at logon to bring a physical display back.
└─> idd_aus.bat
├─> DisplaySwitch.exe /extend (both displays in topology) This fallback is separate from the normal Sunshine display-device workflow. It is only meant as a recovery path after abnormal shutdowns.
├─> 4 second wait
└─> DisplaySwitch.exe /internal (IDD removed, physical monitor stays) ### Sunshine Settings
The tool writes these settings:
```ini
output_name = {selected-display-device-id}
dd_configuration_option = ensure_only_display
dd_resolution_option = auto
dd_refresh_rate_option = auto
dd_config_revert_delay = 3000
``` ```
#### Why /extend before /internal? `ensure_only_display` tells Sunshine to activate the selected display and deactivate the other monitors during the stream.
After `DisplaySwitch /external`, the physical monitor is removed from the Windows display topology. A direct `DisplaySwitch /internal` cannot bring it back — Windows no longer sees it. `/extend` restores both monitors into the topology, then `/internal` removes the IDD monitor again. Resolution and refresh-rate automation are enabled so Sunshine applies the Moonlight client's requested mode to the selected display. Make sure the virtual display driver supports all client modes you want to use, for example `3840x2160 @ 120 Hz`; otherwise Sunshine can fail the display switch.
### Reset
Use `R` in `select_sunshine_monitor.bat` to remove the settings written by this tool:
```ini
output_name
dd_configuration_option
dd_resolution_option
dd_refresh_rate_option
dd_config_revert_delay
```
This returns Sunshine to its default display selection behavior.
### Requirements ### Requirements
- Windows 10/11 - Windows 10/11
- [Sunshine](https://github.com/LizardByte/Sunshine) as streaming server - [Sunshine](https://github.com/LizardByte/Sunshine) as streaming server
- [IDD (Indirect Display Driver)](https://github.com/itsmikethetech/Virtual-Display-Driver) as virtual monitor - [IDD (Indirect Display Driver)](https://github.com/itsmikethetech/Virtual-Display-Driver) or another virtual display driver
--- ---
## Deutsch ## Deutsch
Löst ein Problem beim Game-Streaming mit **Sunshine + Moonlight**: Der physische PC-Monitor wird während des Streams deaktiviert und danach zuverlässig wieder aktiviert. Konfiguriert **Sunshine + Moonlight** so, dass von einem ausgewählten Display-Gerät gestreamt wird. Das ist für Setups mit physischen Monitoren plus IDD-virtuellem Monitor gedacht: Sunshine aktiviert den ausgewählten Monitor für den Stream und deaktiviert die anderen Monitore, solange der Stream aktiv ist.
### Dateien ### Dateien
| Datei | Zweck | | Datei | Zweck |
|---|---| |---|---|
| `idd_an.bat` | Sunshine Do-Command — aktiviert virtuellen IDD-Monitor, deaktiviert physischen Monitor | | `select_sunshine_monitor.bat` | All-in-one-Terminaltool zum Auflisten der Monitore, Auswählen des Streaming-Monitors, Schreiben der Sunshine-Einstellungen und Zurücksetzen |
| `idd_aus.bat` | Sunshine Undo-Command — reaktiviert physischen Monitor | | `register_task.bat` | Optionaler Logon-Fallback nach abnormalem Herunterfahren, während der Stream noch die IDD-only-Topologie verwendet |
| `register_task.bat` | Einmalig ausführen — registriert Logon-Task als Fallback nach Shutdown während Stream |
### Einrichtung ### Einrichtung
#### 1. Sunshine konfigurieren `select_sunshine_monitor.bat` per Doppelklick starten.
In der Sunshine Web-UI unter **Configuration > General**: Das Terminalmenü zeigt alle Sunshine-Display-Geräte mit ID und Status:
| Feld | Wert | - Zahl eingeben, um den Monitor auszuwählen, den Sunshine zum Streamen verwenden soll
|---|---| - `R` eingeben, um die Display-Device-Einstellungen zu entfernen und Sunshine auf Default zurückzusetzen
| Command Preparations — Do | `C:\Projekte\SunshineDisplaySwitch\idd_an.bat` | - `Q` eingeben, um abzubrechen
| Command Preparations — Undo | `C:\Projekte\SunshineDisplaySwitch\idd_aus.bat` |
#### 2. Fallback-Task registrieren (einmalig) Den virtuellen IDD-Monitor auswählen, zum Beispiel `IDD HDR`. Falls Windows Administratorrechte anfordert, bestätigen, damit das Tool die Sunshine-Konfiguration unter `C:\Program Files\Sunshine\config\sunshine.conf` aktualisieren kann.
`register_task.bat` als Administrator ausführen. Registriert `DisplaySwitch /internal` als Logon-Task (`MoonlightTeardown`). Greift, falls der PC während eines Streams heruntergefahren wird und der physische Monitor beim nächsten Start noch deaktiviert ist. Nach dem Speichern Sunshine neu starten, wenn das Tool danach fragt. Eine laufende Moonlight-Sitzung vollständig beenden und anschließend einen neuen Stream starten.
### Funktionsweise ### Optionaler Start-Fallback
``` Sunshine stellt die ursprüngliche Display-Topologie nach einem normalen Streamende wieder her. Wenn Windows aber aus Steam oder einer anderen gestreamten Anwendung heruntergefahren wird, während der Stream noch aktiv ist, bekommt Sunshine eventuell keine Gelegenheit mehr, die IDD-only-Topologie zurückzusetzen. Dann kann Windows beim nächsten Start mit deaktiviertem physischem Monitor hochfahren.
Stream startet
└─> idd_an.bat
└─> DisplaySwitch.exe /external (IDD aktiv, physischer Monitor aus)
Stream endet `register_task.bat` als Administrator ausführen, wenn dieser Fall abgesichert werden soll. Das Skript registriert den Task `MoonlightTeardown` für den aktuellen Benutzer und führt beim Logon `DisplaySwitch.exe /internal` aus, um einen physischen Bildschirm zurückzuholen.
└─> idd_aus.bat
├─> DisplaySwitch.exe /extend (beide Displays in Topologie) Dieser Fallback ist getrennt vom normalen Sunshine-Display-Device-Workflow. Er ist nur als Recovery-Pfad nach abnormalem Herunterfahren gedacht.
├─> 4 Sekunden warten
└─> DisplaySwitch.exe /internal (IDD entfernt, physischer Monitor bleibt) ### Sunshine-Einstellungen
Das Tool schreibt diese Einstellungen:
```ini
output_name = {ausgewaehlte-display-device-id}
dd_configuration_option = ensure_only_display
dd_resolution_option = auto
dd_refresh_rate_option = auto
dd_config_revert_delay = 3000
``` ```
#### Warum /extend vor /internal? `ensure_only_display` weist Sunshine an, das ausgewählte Display zu aktivieren und die anderen Monitore während des Streams zu deaktivieren.
Nach `DisplaySwitch /external` wird der physische Monitor aus der Windows Display-Topologie entfernt. Ein direktes `DisplaySwitch /internal` würde ihn nicht zurückholen — er ist für Windows nicht mehr sichtbar. `/extend` bringt beide Monitore zurück in die Topologie, danach entfernt `/internal` den IDD-Monitor wieder. Auflösungs- und Hz-Automatik sind aktiviert, damit Sunshine den vom Moonlight-Client angeforderten Modus auf dem ausgewählten Display setzt. Der virtuelle Display-Treiber muss alle gewünschten Client-Modi unterstützen, zum Beispiel `3840x2160 @ 120 Hz`; andernfalls kann Sunshine die Display-Umschaltung abbrechen.
### Zurücksetzen
Mit `R` in `select_sunshine_monitor.bat` werden die von diesem Tool geschriebenen Einstellungen entfernt:
```ini
output_name
dd_configuration_option
dd_resolution_option
dd_refresh_rate_option
dd_config_revert_delay
```
Damit verwendet Sunshine wieder sein Standardverhalten für die Display-Auswahl.
### Voraussetzungen ### Voraussetzungen
- Windows 10/11 - Windows 10/11
- [Sunshine](https://github.com/LizardByte/Sunshine) als Streaming-Server - [Sunshine](https://github.com/LizardByte/Sunshine) als Streaming-Server
- [IDD (Indirect Display Driver)](https://github.com/itsmikethetech/Virtual-Display-Driver) als virtueller Monitor - [IDD (Indirect Display Driver)](https://github.com/itsmikethetech/Virtual-Display-Driver) oder ein anderer virtueller Display-Treiber
-2
View File
@@ -1,2 +0,0 @@
@echo off
DisplaySwitch.exe /external
-4
View File
@@ -1,4 +0,0 @@
@echo off
DisplaySwitch /extend
timeout /t 4
DisplaySwitch /internal
+623
View File
@@ -0,0 +1,623 @@
@echo off
setlocal
set "SDS_SELF=%~f0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Sta -Command "$path=$env:SDS_SELF; $raw=Get-Content -LiteralPath $path -Raw; $marker='### POWERSHELL ###'; $index=$raw.LastIndexOf($marker); if($index -lt 0){throw 'PowerShell block not found.'}; $code=$raw.Substring($index + $marker.Length); Invoke-Expression $code"
exit /b %ERRORLEVEL%
### POWERSHELL ###
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$SunshineDir = if ([string]::IsNullOrWhiteSpace($env:SDS_SUNSHINE_DIR)) { Join-Path $env:ProgramFiles 'Sunshine' } else { $env:SDS_SUNSHINE_DIR }
$DeviceId = $env:SDS_DEVICE_ID
$ListOnly = $env:SDS_LIST_ONLY -eq '1'
$NoRestart = $env:SDS_NO_RESTART -eq '1'
$Silent = $env:SDS_SILENT -eq '1'
$Action = $env:SDS_ACTION
$BatchPath = $env:SDS_SELF
function Test-IsAdministrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Test-CanWriteFile {
param([string]$Path)
try {
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
$stream.Close()
return $true
}
catch {
return $false
}
}
function Get-JsonArrayAfterMarker {
param(
[string]$Text,
[string]$Marker
)
$markerIndex = $Text.LastIndexOf($Marker, [StringComparison]::OrdinalIgnoreCase)
if ($markerIndex -lt 0) {
return $null
}
$start = $Text.IndexOf('[', $markerIndex + $Marker.Length)
if ($start -lt 0) {
return $null
}
$depth = 0
$inString = $false
$escaped = $false
for ($i = $start; $i -lt $Text.Length; $i++) {
$char = $Text[$i]
if ($inString) {
if ($escaped) {
$escaped = $false
continue
}
if ($char -eq '\') {
$escaped = $true
continue
}
if ($char -eq '"') {
$inString = $false
}
continue
}
if ($char -eq '"') {
$inString = $true
continue
}
if ($char -eq '[') {
$depth++
continue
}
if ($char -eq ']') {
$depth--
if ($depth -eq 0) {
return $Text.Substring($start, $i - $start + 1)
}
}
}
return $null
}
function Format-Rational {
param($Rational)
$rationalValue = Get-ObjectProperty -Object $Rational -Name 'value'
if ($null -eq $rationalValue) {
return ''
}
$denominator = [double](Get-ObjectProperty -Object $rationalValue -Name 'denominator')
if ($denominator -eq 0) {
return ''
}
$value = [double](Get-ObjectProperty -Object $rationalValue -Name 'numerator') / $denominator
$formatted = ('{0:N2}' -f $value).TrimEnd('0').TrimEnd((Get-Culture).NumberFormat.NumberDecimalSeparator)
return "$formatted Hz"
}
function Get-ObjectProperty {
param(
$Object,
[string]$Name
)
if ($null -eq $Object) {
return $null
}
$property = $Object.PSObject.Properties[$Name]
if ($null -eq $property) {
return $null
}
return $property.Value
}
function ConvertTo-DisplayRows {
param([object[]]$Devices)
if ($Devices.Count -eq 1 -and $Devices[0] -is [Array]) {
$Devices = $Devices[0]
}
$rows = @()
foreach ($device in $Devices) {
$resolution = ''
$refreshRate = ''
$primary = $false
$active = $false
$info = Get-ObjectProperty -Object $device -Name 'info'
if ($null -ne $info) {
$active = $true
$resolutionInfo = Get-ObjectProperty -Object $info -Name 'resolution'
if ($null -ne $resolutionInfo) {
$resolution = '{0}x{1}' -f (Get-ObjectProperty -Object $resolutionInfo -Name 'width'), (Get-ObjectProperty -Object $resolutionInfo -Name 'height')
}
$refreshRate = Format-Rational -Rational (Get-ObjectProperty -Object $info -Name 'refresh_rate')
$primaryInfo = Get-ObjectProperty -Object $info -Name 'primary'
if ($null -ne $primaryInfo) {
$primary = [bool]$primaryInfo
}
}
$edid = ''
$deviceEdid = Get-ObjectProperty -Object $device -Name 'edid'
if ($null -ne $deviceEdid) {
$edidParts = @()
$manufacturerId = Get-ObjectProperty -Object $deviceEdid -Name 'manufacturer_id'
$productCode = Get-ObjectProperty -Object $deviceEdid -Name 'product_code'
$serialNumber = Get-ObjectProperty -Object $deviceEdid -Name 'serial_number'
if ($manufacturerId) { $edidParts += $manufacturerId }
if ($productCode) { $edidParts += $productCode }
if ($null -ne $serialNumber) { $edidParts += "SN:$serialNumber" }
$edid = $edidParts -join ' '
}
$rows += [pscustomobject]@{
Name = [string](Get-ObjectProperty -Object $device -Name 'friendly_name')
DeviceId = [string](Get-ObjectProperty -Object $device -Name 'device_id')
DisplayName = [string](Get-ObjectProperty -Object $device -Name 'display_name')
Active = $active
Primary = $primary
Resolution = $resolution
RefreshRate = $refreshRate
Edid = $edid
}
}
return $rows
}
function Get-SunshineDisplayDevicesDirect {
param([string]$InstallDir)
$sunshineExe = Join-Path $InstallDir 'sunshine.exe'
if (-not (Test-Path -LiteralPath $sunshineExe)) {
throw "Sunshine executable not found: $sunshineExe"
}
$stdoutPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.out' -f ([guid]::NewGuid()))
$stderrPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.err' -f ([guid]::NewGuid()))
$probeLogPath = Join-Path $env:TEMP ('sunshine-display-probe-{0}.log' -f ([guid]::NewGuid()))
$process = $null
function Stop-ProbeProcess {
param($Process)
if ($null -eq $Process -or $Process.HasExited) {
return
}
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
try {
$Process.WaitForExit(2000) | Out-Null
}
catch {
}
}
try {
$arguments = @('-1', "log_path=$probeLogPath")
$process = Start-Process -FilePath $sunshineExe -ArgumentList $arguments -WorkingDirectory $InstallDir -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru
$deadline = (Get-Date).AddSeconds(10)
$json = $null
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 200
if (Test-Path -LiteralPath $stdoutPath) {
$text = Get-Content -LiteralPath $stdoutPath -Raw
$json = Get-JsonArrayAfterMarker -Text $text -Marker 'Currently available display devices:'
if (-not [string]::IsNullOrWhiteSpace($json)) {
break
}
}
if ($process.HasExited) {
break
}
}
Stop-ProbeProcess -Process $process
if ([string]::IsNullOrWhiteSpace($json)) {
$stdout = if (Test-Path -LiteralPath $stdoutPath) { Get-Content -LiteralPath $stdoutPath -Raw } else { '' }
$stderr = if (Test-Path -LiteralPath $stderrPath) { Get-Content -LiteralPath $stderrPath -Raw } else { '' }
throw "Sunshine did not return a display-device list.`n`nstdout:`n$stdout`n`nstderr:`n$stderr"
}
return ConvertTo-DisplayRows -Devices @($json | ConvertFrom-Json)
}
finally {
Stop-ProbeProcess -Process $process
Remove-Item -LiteralPath $stdoutPath, $stderrPath, $probeLogPath -ErrorAction SilentlyContinue
}
}
function Set-ConfigValue {
param(
[string[]]$Lines,
[string]$Key,
[string]$Value
)
$pattern = '^\s*' + [regex]::Escape($Key) + '\s*='
$replacement = "$Key = $Value"
$found = $false
for ($i = 0; $i -lt $Lines.Count; $i++) {
if ($Lines[$i] -match $pattern) {
$Lines[$i] = $replacement
$found = $true
break
}
}
if (-not $found) {
$Lines += $replacement
}
return ,$Lines
}
function Remove-ConfigValues {
param(
[string[]]$Lines,
[string[]]$Keys
)
$patterns = @($Keys | ForEach-Object { '^\s*' + [regex]::Escape($_) + '\s*=' })
$kept = @()
foreach ($line in $Lines) {
$remove = $false
foreach ($pattern in $patterns) {
if ($line -match $pattern) {
$remove = $true
break
}
}
if (-not $remove) {
$kept += $line
}
}
return ,$kept
}
function Remove-LegacyDisplayPrepCommands {
param([string[]]$Lines)
$pattern = '^\s*global_prep_cmd\s*=\s*(.+?)\s*$'
for ($i = 0; $i -lt $Lines.Count; $i++) {
if ($Lines[$i] -notmatch $pattern) {
continue
}
$json = $Matches[1]
try {
$commands = @($json | ConvertFrom-Json)
if ($commands.Count -eq 1 -and $commands[0] -is [Array]) {
$commands = $commands[0]
}
}
catch {
return ,$Lines
}
$kept = @()
foreach ($command in $commands) {
$do = if ($null -ne $command.do) { [string]$command.do } else { '' }
$undo = if ($null -ne $command.undo) { [string]$command.undo } else { '' }
$combined = "$do $undo"
$isLegacyDisplayCommand = $combined -match '(?i)(idd_an\.bat|idd_aus\.bat|DisplaySwitch(\.exe)?\s+(/internal|/external|/extend|/clone|[1234]))'
if (-not $isLegacyDisplayCommand) {
$kept += $command
}
}
$Lines[$i] = 'global_prep_cmd = ' + (ConvertTo-Json -InputObject $kept -Compress -Depth 10)
return ,$Lines
}
return ,$Lines
}
function Save-SunshineConfig {
param(
[string]$ConfigPath,
[pscustomobject]$SelectedDisplay
)
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "Sunshine config not found: $ConfigPath"
}
$backupPath = '{0}.bak-{1}' -f $ConfigPath, (Get-Date -Format 'yyyyMMdd-HHmmss')
Copy-Item -LiteralPath $ConfigPath -Destination $backupPath
$lines = @(Get-Content -LiteralPath $ConfigPath)
$lines = Remove-LegacyDisplayPrepCommands -Lines $lines
$lines = Set-ConfigValue -Lines $lines -Key 'output_name' -Value $SelectedDisplay.DeviceId
$lines = Set-ConfigValue -Lines $lines -Key 'dd_configuration_option' -Value 'ensure_only_display'
$lines = Set-ConfigValue -Lines $lines -Key 'dd_resolution_option' -Value 'auto'
$lines = Set-ConfigValue -Lines $lines -Key 'dd_refresh_rate_option' -Value 'auto'
$lines = Set-ConfigValue -Lines $lines -Key 'dd_config_revert_delay' -Value '3000'
Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding UTF8
return $backupPath
}
function Reset-SunshineDisplayConfig {
param([string]$ConfigPath)
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "Sunshine config not found: $ConfigPath"
}
$backupPath = '{0}.bak-{1}' -f $ConfigPath, (Get-Date -Format 'yyyyMMdd-HHmmss')
Copy-Item -LiteralPath $ConfigPath -Destination $backupPath
$keysToRemove = @(
'output_name'
'dd_configuration_option'
'dd_resolution_option'
'dd_refresh_rate_option'
'dd_config_revert_delay'
)
$lines = @(Get-Content -LiteralPath $ConfigPath)
$lines = Remove-ConfigValues -Lines $lines -Keys $keysToRemove
Set-Content -LiteralPath $ConfigPath -Value $lines -Encoding UTF8
return $backupPath
}
function Show-MonitorMenu {
param([object[]]$Devices)
Write-Host ''
Write-Host 'Sunshine Streaming-Monitor auswaehlen'
Write-Host '===================================='
Write-Host ''
for ($i = 0; $i -lt $Devices.Count; $i++) {
$device = $Devices[$i]
$state = if ($device.Active) { 'aktiv' } else { 'inaktiv' }
$primary = if ($device.Primary) { ', primaer' } else { '' }
$displayName = if ([string]::IsNullOrWhiteSpace($device.DisplayName)) { '-' } else { $device.DisplayName }
$resolution = if ([string]::IsNullOrWhiteSpace($device.Resolution)) { '-' } else { $device.Resolution }
$refreshRate = if ([string]::IsNullOrWhiteSpace($device.RefreshRate)) { '-' } else { $device.RefreshRate }
$edid = if ([string]::IsNullOrWhiteSpace($device.Edid)) { '-' } else { $device.Edid }
Write-Host ('[{0}] {1}' -f ($i + 1), $device.Name)
Write-Host (' ID : {0}' -f $device.DeviceId)
Write-Host (' DisplayName : {0}' -f $displayName)
Write-Host (' Status : {0}{1}' -f $state, $primary)
Write-Host (' Modus : {0}, {1}' -f $resolution, $refreshRate)
Write-Host (' EDID : {0}' -f $edid)
Write-Host ''
}
Write-Host '[R] Sunshine-Monitor-Einstellungen auf Default zuruecksetzen'
Write-Host '[Q] Abbrechen'
Write-Host ''
while ($true) {
$choice = (Read-Host 'Auswahl').Trim()
if ($choice -match '^(q|Q)$') {
return [pscustomobject]@{ Action = 'quit'; Display = $null }
}
if ($choice -match '^(r|R)$') {
return [pscustomobject]@{ Action = 'reset'; Display = $null }
}
$number = 0
if ([int]::TryParse($choice, [ref]$number) -and $number -ge 1 -and $number -le $Devices.Count) {
return [pscustomobject]@{ Action = 'select'; Display = $Devices[$number - 1] }
}
Write-Host 'Ungueltige Auswahl. Bitte Zahl, R oder Q eingeben.'
}
}
function Pause-IfInteractive {
if (-not $Silent -and -not $ListOnly -and $env:SDS_NO_PAUSE -ne '1') {
Write-Host ''
Read-Host 'Enter zum Schliessen' | Out-Null
}
}
function Restart-Elevated {
param(
[string]$RequestedAction,
[string]$SelectedDeviceId
)
$commands = @(
('set "SDS_ACTION={0}"' -f $RequestedAction)
('set "SDS_SUNSHINE_DIR={0}"' -f $SunshineDir)
('set "SDS_NO_RESTART={0}"' -f ($(if ($NoRestart) { '1' } else { '0' })))
)
if (-not [string]::IsNullOrWhiteSpace($SelectedDeviceId)) {
$commands += ('set "SDS_DEVICE_ID={0}"' -f $SelectedDeviceId)
}
if (-not [string]::IsNullOrWhiteSpace($env:SDS_CONFIG_PATH)) {
$commands += ('set "SDS_CONFIG_PATH={0}"' -f $env:SDS_CONFIG_PATH)
}
if ($Silent) {
$commands += 'set "SDS_SILENT=1"'
}
$commands += ('"{0}"' -f $BatchPath)
$command = $commands -join ' && '
Start-Process -FilePath $env:ComSpec -ArgumentList ('/c ' + $command) -Verb RunAs
}
function Restart-SunshineServiceIfRequested {
param([switch]$NoRestart)
if ($NoRestart) {
return
}
$answer = (Read-Host 'Sunshine jetzt neu starten, damit die Aenderung sofort aktiv wird? [j/N]').Trim()
if ($answer -notmatch '^(j|J|y|Y)$') {
return
}
$services = @(Get-Service | Where-Object {
$_.Name -like '*Sunshine*' -or $_.DisplayName -like '*Sunshine*'
})
if ($services.Count -eq 0) {
Write-Host 'Kein Sunshine-Dienst gefunden. Starte Sunshine manuell neu.'
return
}
foreach ($service in $services) {
Restart-Service -Name $service.Name -Force
}
Write-Host 'Sunshine wurde neu gestartet.'
}
$configPath = if ([string]::IsNullOrWhiteSpace($env:SDS_CONFIG_PATH)) { Join-Path $SunshineDir 'config\sunshine.conf' } else { $env:SDS_CONFIG_PATH }
try {
if ($Action -eq 'reset') {
if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) {
Restart-Elevated -RequestedAction 'reset' -SelectedDeviceId ''
exit
}
$backupPath = Reset-SunshineDisplayConfig -ConfigPath $configPath
if ($Silent) {
Write-Output ("Reset: Sunshine display settings removed")
Write-Output ("Config: {0}" -f $configPath)
Write-Output ("Backup: {0}" -f $backupPath)
exit
}
Write-Host ''
Write-Host 'Sunshine-Monitor-Einstellungen wurden auf Default zurueckgesetzt.'
Write-Host ("Config : {0}" -f $configPath)
Write-Host ("Backup : {0}" -f $backupPath)
Write-Host ''
Restart-SunshineServiceIfRequested -NoRestart:$NoRestart
Pause-IfInteractive
exit
}
if ([string]::IsNullOrWhiteSpace($DeviceId)) {
$devices = @(Get-SunshineDisplayDevicesDirect -InstallDir $SunshineDir)
}
else {
$devices = @(Get-SunshineDisplayDevicesDirect -InstallDir $SunshineDir)
}
if ($ListOnly) {
$devices | Format-Table Name, DeviceId, DisplayName, Active, Primary, Resolution, RefreshRate -AutoSize
exit
}
if ([string]::IsNullOrWhiteSpace($DeviceId)) {
$menuResult = Show-MonitorMenu -Devices $devices
if ($menuResult.Action -eq 'quit') {
Write-Host 'Abgebrochen.'
Pause-IfInteractive
exit
}
if ($menuResult.Action -eq 'reset') {
if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) {
Restart-Elevated -RequestedAction 'reset' -SelectedDeviceId ''
exit
}
$backupPath = Reset-SunshineDisplayConfig -ConfigPath $configPath
Write-Host ''
Write-Host 'Sunshine-Monitor-Einstellungen wurden auf Default zurueckgesetzt.'
Write-Host ("Config : {0}" -f $configPath)
Write-Host ("Backup : {0}" -f $backupPath)
Write-Host ''
Restart-SunshineServiceIfRequested -NoRestart:$NoRestart
Pause-IfInteractive
exit
}
$selectedDisplay = $menuResult.Display
}
else {
$selectedDisplay = $devices | Where-Object { $_.DeviceId -ieq $DeviceId } | Select-Object -First 1
if ($null -eq $selectedDisplay) {
throw "Display device id not found in Sunshine output: $DeviceId"
}
}
if ($null -eq $selectedDisplay) {
exit
}
if (-not (Test-CanWriteFile -Path $configPath) -and -not (Test-IsAdministrator)) {
Restart-Elevated -RequestedAction 'select' -SelectedDeviceId $selectedDisplay.DeviceId
exit
}
$backupPath = Save-SunshineConfig -ConfigPath $configPath -SelectedDisplay $selectedDisplay
if ($Silent) {
Write-Output ("Selected: {0}" -f $selectedDisplay.Name)
Write-Output ("DeviceId: {0}" -f $selectedDisplay.DeviceId)
Write-Output ("Config: {0}" -f $configPath)
Write-Output ("Backup: {0}" -f $backupPath)
exit
}
Write-Host ''
Write-Host 'Sunshine-Konfiguration gespeichert.'
Write-Host ("Ausgewaehlt : {0}" -f $selectedDisplay.Name)
Write-Host ("ID : {0}" -f $selectedDisplay.DeviceId)
Write-Host ("Config : {0}" -f $configPath)
Write-Host ("Backup : {0}" -f $backupPath)
Write-Host ''
Restart-SunshineServiceIfRequested -NoRestart:$NoRestart
Pause-IfInteractive
}
catch {
Write-Error $_
Pause-IfInteractive
exit 1
}