mirror of
https://github.com/Andre-Bzk/SunshineDisplaySwitch.git
synced 2026-11-15 09:33:11 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
995cf19fc9 |
@@ -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
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
@echo off
|
|
||||||
DisplaySwitch.exe /external
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
DisplaySwitch /extend
|
|
||||||
timeout /t 4
|
|
||||||
DisplaySwitch /internal
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user