From f23d5afaa3dc335da49824c4bb6f1c0feef89296 Mon Sep 17 00:00:00 2001 From: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri, 1 May 2026 18:48:36 +0200 Subject: [PATCH] first commit --- .gitignore | 1 + README.md | 44 ++++++++ scripts/config.ps1 | 1 + scripts/setup-wsl-airplay.ps1 | 201 ++++++++++++++++++++++++++++++++++ scripts/start-airplay.ps1 | 161 +++++++++++++++++++++++++++ wsl/setup-uxplay.sh | 60 ++++++++++ 6 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 scripts/config.ps1 create mode 100644 scripts/setup-wsl-airplay.ps1 create mode 100644 scripts/start-airplay.ps1 create mode 100644 wsl/setup-uxplay.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21b49c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +scripts/uxplay.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f9c5b8 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# iOS/iPadOS Screen Mirroring to Windows (AirPlay via WSL) + +This project sets up UxPlay (open-source AirPlay receiver) inside WSL2 and launches it as a window using WSLg. +Your iPhone/iPad can mirror to that window. + +## Requirements +- Windows 11 with WSL. +- iPhone/iPad and PC on the same Wi-Fi. + +## One-time setup +1. Open PowerShell in this folder and run: + scripts\setup-wsl-airplay.ps1 +2. If the script asks you to restart WSL, run: + wsl --shutdown + Then rerun the setup script. + +## Start mirroring +scripts\start-airplay.ps1 + +On the iPhone/iPad: Control Center -> Screen Mirroring -> select the server name. + +## Tips +- If the device does not see the receiver, verify WSL mirrored networking was enabled and allow UDP 5353 in Windows Firewall. + +- If it appears but fails to connect, open UDP/TCP 35000-35002 in Windows Firewall (this is the default port range). + +- Default is low latency with hardware decoding. + +- If audio crackles or drifts, try: + scripts\start-airplay.ps1 -UseVsync + scripts\start-airplay.ps1 -UseVsync -SoftwareDecode + +## Opening the ports locally + +Run in an elevated PowerShell: + +New-NetFirewallRule -DisplayName "WSL AirPlay TCP" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 35000-35002 +New-NetFirewallRule -DisplayName "WSL AirPlay UDP" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 35000-35002 +New-NetFirewallRule -DisplayName "WSL AirPlay mDNS" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 5353 + +## What gets installed in WSL +- UxPlay +- Avahi (mDNS/DNS-SD) +- GStreamer plugins for audio/video diff --git a/scripts/config.ps1 b/scripts/config.ps1 new file mode 100644 index 0000000..9622716 --- /dev/null +++ b/scripts/config.ps1 @@ -0,0 +1 @@ +$DefaultName = "winniepc" diff --git a/scripts/setup-wsl-airplay.ps1 b/scripts/setup-wsl-airplay.ps1 new file mode 100644 index 0000000..3f1e92d --- /dev/null +++ b/scripts/setup-wsl-airplay.ps1 @@ -0,0 +1,201 @@ +param( + [string]$Distro = "Ubuntu" +) + +$ErrorActionPreference = "Stop" + +function Write-Info { + param([string]$Message) + Write-Host $Message +} + +function Ensure-WslInstalled { + if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { + Write-Warning "WSL is not installed. Install it with: wsl --install -d $Distro" + throw "WSL is required." + } +} + +function Ensure-WslDistro { + $distros = @() + try { + $distros = wsl -l -q 2>$null + } catch { + $distros = @() + } + + if (-not $distros -or ($distros -notcontains $Distro)) { + Write-Warning "WSL distro '$Distro' is not installed. Install it with: wsl --install -d $Distro" + throw "WSL distro not found." + } +} + +function Ensure-WslConfig { + $configPath = Join-Path $env:USERPROFILE ".wslconfig" + $lines = [System.Collections.Generic.List[string]]::new() + + if (Test-Path $configPath) { + $content = Get-Content -Path $configPath -ErrorAction SilentlyContinue + foreach ($line in $content) { + $null = $lines.Add([string]$line) + } + } + + $wsl2Index = $lines.FindIndex({ param($l) $l -match '^\s*\[wsl2\]\s*$' }) + + if ($wsl2Index -lt 0) { + if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -ne "") { + $lines.Add("") + } + $lines.Add("[wsl2]") + $lines.Add("networkingMode=mirrored") + $lines.Add("guiApplications=true") + } else { + $blockEnd = $lines.Count + for ($i = $wsl2Index + 1; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match '^\s*\[.+\]\s*$') { + $blockEnd = $i + break + } + } + + $netIndex = -1 + $guiIndex = -1 + for ($i = $wsl2Index + 1; $i -lt $blockEnd; $i++) { + if ($lines[$i] -match '^\s*networkingMode=') { $netIndex = $i } + if ($lines[$i] -match '^\s*guiApplications=') { $guiIndex = $i } + } + + if ($netIndex -ge 0) { + $lines[$netIndex] = "networkingMode=mirrored" + } else { + $lines.Insert($blockEnd, "networkingMode=mirrored") + $blockEnd++ + } + + if ($guiIndex -ge 0) { + $lines[$guiIndex] = "guiApplications=true" + } else { + $lines.Insert($blockEnd, "guiApplications=true") + } + } + + Set-Content -Path $configPath -Value $lines -Encoding ASCII +} + +function New-StartMenuShortcut { + param( + [string]$ShortcutName, + [string]$ScriptPath, + [string]$WorkingDirectory + ) + + $startMenuDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs" + $shortcutPath = Join-Path $startMenuDir "$ShortcutName.lnk" + $iconPath = Join-Path $env:SystemRoot "System32\imageres.dll" + + $shell = New-Object -ComObject WScript.Shell + $shortcut = $shell.CreateShortcut($shortcutPath) + $shortcut.TargetPath = (Get-Command powershell.exe).Source + $shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`"" + $shortcut.WorkingDirectory = $WorkingDirectory + $shortcut.IconLocation = "$iconPath,15" + $shortcut.WindowStyle = 1 + $shortcut.Save() +} + +function Test-IsAdmin { + $current = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($current) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Add-FirewallRules { + $rulePrefix = "ScreenShare AirPlay" + New-NetFirewallRule -DisplayName "$rulePrefix TCP" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 35000-35002 | Out-Null + New-NetFirewallRule -DisplayName "$rulePrefix UDP" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 35000-35002 | Out-Null + New-NetFirewallRule -DisplayName "$rulePrefix mDNS" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 5353 | Out-Null +} + +function Write-Config { + param( + [string]$ConfigPath, + [string]$DefaultName + ) + + $lines = @() + if (-not [string]::IsNullOrWhiteSpace($DefaultName)) { + $safeName = $DefaultName.Replace('"', '`"') + $lines += "`$DefaultName = `"$safeName`"" + } + + Set-Content -Path $ConfigPath -Value $lines -Encoding ASCII +} + +Ensure-WslInstalled +Ensure-WslDistro + +Write-Info "Updating .wslconfig to enable mirrored networking and GUI apps..." +Ensure-WslConfig + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") +$wslScriptWindows = Resolve-Path (Join-Path $repoRoot "wsl\setup-uxplay.sh") +$wslScriptWindowsPath = $wslScriptWindows.Path +$wslScriptWindowsForWsl = $wslScriptWindowsPath -replace "\\", "/" + +$wslScriptLinux = (wsl -d $Distro -- wslpath -a "$wslScriptWindowsForWsl" 2>$null) +if (-not $wslScriptLinux) { + throw "Failed to resolve WSL path for setup script: $wslScriptWindowsPath" +} +$wslScriptLinux = $wslScriptLinux.Trim() + +Write-Info "Running WSL setup script..." +$bashCmd = "chmod +x '$wslScriptLinux'; '$wslScriptLinux'" +& wsl -d $Distro -- bash -lc $bashCmd + +Write-Info "Restarting WSL to apply systemd and networking changes..." +& wsl --shutdown + +Write-Info "Attempting to enable Avahi after restart (may prompt for sudo password)..." +$enableCmd = "if command -v systemctl >/dev/null 2>&1; then sudo systemctl enable --now avahi-daemon; else echo 'systemctl not available'; fi" +& wsl -d $Distro -- bash -lc $enableCmd + +$startScriptPath = Resolve-Path (Join-Path $repoRoot "scripts\start-airplay.ps1") +$startShortcutName = "ScreenShare" +$prompt = "Create Start Menu shortcut for $startShortcutName? (Y/N) [Y]" +$response = Read-Host $prompt +if ([string]::IsNullOrWhiteSpace($response) -or $response -match '^[Yy]') { + New-StartMenuShortcut -ShortcutName $startShortcutName -ScriptPath $startScriptPath.Path -WorkingDirectory $repoRoot.Path + Write-Info "Start Menu shortcut created: $startShortcutName" +} else { + Write-Info "Skipped Start Menu shortcut." +} + +$firewallPrompt = "Add Windows Firewall rules for ports 35000-35002 and 5353? (Y/N) [Y]" +$firewallResponse = Read-Host $firewallPrompt +if ([string]::IsNullOrWhiteSpace($firewallResponse) -or $firewallResponse -match '^[Yy]') { + if (Test-IsAdmin) { + Add-FirewallRules + Write-Info "Firewall rules added." + } else { + Write-Info "Skipping firewall rules (requires an elevated PowerShell)." + } +} else { + Write-Info "Skipped firewall rules." +} + +$configPath = Join-Path $repoRoot "scripts\config.ps1" +$namePrompt = "Set default AirPlay name? (Y/N) [Y]" +$nameResponse = Read-Host $namePrompt +if ([string]::IsNullOrWhiteSpace($nameResponse) -or $nameResponse -match '^[Yy]') { + $defaultName = Read-Host "Default name (blank keeps ScreenShare)" + if ([string]::IsNullOrWhiteSpace($defaultName)) { + $defaultName = "ScreenShare" + } + Write-Config -ConfigPath $configPath -DefaultName $defaultName + Write-Info "Default name set to: $defaultName" +} else { + Write-Info "Skipped default name configuration." +} + +Write-Info "Setup complete. You can now run scripts\\start-airplay.ps1" diff --git a/scripts/start-airplay.ps1 b/scripts/start-airplay.ps1 new file mode 100644 index 0000000..040683b --- /dev/null +++ b/scripts/start-airplay.ps1 @@ -0,0 +1,161 @@ +param( + [string]$Distro = "Ubuntu", + [string]$Name = "ScreenShare", + [switch]$NoHostSuffix, + [switch]$LowLatency, + [switch]$UseVsync, + [switch]$HardwareDecode, + [switch]$SoftwareDecode, + [string]$VideoSink = "gtksink", + [switch]$AutoVideo, + [int]$PortBase = 35000, + [switch]$Quiet, + [string]$LogPath = "" +) + +$ErrorActionPreference = "Stop" + +$configPath = Join-Path $PSScriptRoot "config.ps1" +if (Test-Path $configPath) { + try { + . $configPath + } catch { + } +} + +if (-not $PSBoundParameters.ContainsKey("Name") -and $DefaultName) { + $Name = $DefaultName +} + +if ([string]::IsNullOrWhiteSpace($Name)) { + $Name = "ScreenShare" +} + +function Reset-OutputCursor { + try { + if ([Console]::CursorLeft -ne 0) { + [Console]::WriteLine("") + } + [Console]::CursorLeft = 0 + } catch { + try { + $pos = $Host.UI.RawUI.CursorPosition + if ($pos.X -ne 0) { + Write-Host "" + } + $Host.UI.RawUI.CursorPosition = New-Object Management.Automation.Host.Coordinates 0 $Host.UI.RawUI.CursorPosition.Y + } catch { + Write-Host "" + } + } +} + +try { + if ($Host.UI.RawUI.CursorPosition.X -ne 0) { + Write-Host "" + } +} catch { +} + +if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { + throw "WSL is not installed. Run scripts\\setup-wsl-airplay.ps1 first." +} + +$distros = @() +try { + $distros = wsl -l -q 2>$null +} catch { + $distros = @() +} + +if (-not $distros -or ($distros -notcontains $Distro)) { + throw "WSL distro '$Distro' not found. Run scripts\\setup-wsl-airplay.ps1 first." +} + +$uxplayPath = (wsl -d $Distro -- bash -lc "command -v uxplay" 2>$null).Trim() +if (-not $uxplayPath) { + throw "UxPlay is not installed in WSL. Run scripts\\setup-wsl-airplay.ps1 first." +} + +if ($PortBase -ne 0) { + if ($PortBase -lt 1024 -or $PortBase -gt 65533) { + throw "PortBase must be between 1024 and 65533." + } +} + +$escapedName = $Name -replace '"', '\\"' +$cmd = "uxplay -n `"$escapedName`"" + +if ($NoHostSuffix) { + $cmd += " -nh" +} + +$useLowLatency = $true +if ($UseVsync) { + $useLowLatency = $false +} +if ($LowLatency) { + $useLowLatency = $true +} + +$useHardwareDecode = $true +if ($SoftwareDecode) { + $useHardwareDecode = $false +} +if ($HardwareDecode) { + $useHardwareDecode = $true +} + +if (-not $useHardwareDecode) { + $cmd += " -avdec" +} + +if ($useLowLatency) { + $cmd += " -vsync no" +} + +if (-not $AutoVideo -and $VideoSink) { + $cmd += " -vs $VideoSink" +} + +if ($PortBase -ne 0) { + $cmd += " -p $PortBase" +} + +Reset-OutputCursor +[Console]::WriteLine("Starting ScreenShare. Logs follow below.") + +$redirectLogs = $Quiet +if ($redirectLogs -and [string]::IsNullOrWhiteSpace($LogPath)) { + $LogPath = Join-Path $PSScriptRoot "screenshare.log" +} + +$nativePrefSet = $false +$nativePrefValue = $null +if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { + $nativePrefSet = $true + $nativePrefValue = $PSNativeCommandUseErrorActionPreference + $PSNativeCommandUseErrorActionPreference = $false +} + +$savedErrorActionPreference = $ErrorActionPreference +if ($redirectLogs) { + $ErrorActionPreference = "SilentlyContinue" +} else { + $ErrorActionPreference = "Continue" +} + +try { + if ($redirectLogs) { + & wsl -d $Distro -- bash -lc $cmd 1>$LogPath 2>&1 + Reset-OutputCursor + [Console]::WriteLine("ScreenShare logs written to: $LogPath") + } else { + & wsl -d $Distro -- bash -lc $cmd + } +} finally { + $ErrorActionPreference = $savedErrorActionPreference + if ($nativePrefSet) { + $PSNativeCommandUseErrorActionPreference = $nativePrefValue + } +} diff --git a/wsl/setup-uxplay.sh b/wsl/setup-uxplay.sh new file mode 100644 index 0000000..780e5e6 --- /dev/null +++ b/wsl/setup-uxplay.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Configuring WSL for systemd (required for Avahi)" +if [ ! -f /etc/wsl.conf ]; then + printf "[boot]\nsystemd=true\n" | sudo tee /etc/wsl.conf >/dev/null +else + if ! grep -q '^\[boot\]' /etc/wsl.conf; then + printf "\n[boot]\nsystemd=true\n" | sudo tee -a /etc/wsl.conf >/dev/null + elif ! grep -q '^systemd=' /etc/wsl.conf; then + sudo sed -i '/^\[boot\]$/a systemd=true' /etc/wsl.conf + fi +fi + +echo "==> Installing dependencies" +sudo apt-get update +sudo apt-get install -y \ + build-essential \ + cmake \ + pkg-config \ + git \ + libssl-dev \ + libplist-dev \ + libavahi-compat-libdnssd-dev \ + avahi-daemon \ + avahi-utils \ + gstreamer1.0-tools \ + gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly \ + gstreamer1.0-libav \ + gstreamer1.0-gtk3 + +echo "==> Installing UxPlay" +if apt-cache show uxplay >/dev/null 2>&1; then + sudo apt-get install -y uxplay +else + workdir="$HOME/uxplay-src" + if [ ! -d "$workdir/.git" ]; then + rm -rf "$workdir" + git clone https://github.com/FDH2/UxPlay "$workdir" + else + git -C "$workdir" pull --ff-only + fi + + cmake -S "$workdir" -B "$workdir/build" + cmake --build "$workdir/build" -j + sudo cmake --install "$workdir/build" +fi + +if command -v systemctl >/dev/null 2>&1 && ps -p 1 -o comm= | grep -q systemd; then + echo "==> Enabling Avahi" + sudo systemctl enable --now avahi-daemon +else + echo "NOTE: systemd is not active yet. After WSL restart, run:" + echo " sudo systemctl enable --now avahi-daemon" +fi + +echo "==> Done"