diff --git a/.gitattributes b/.gitattributes index f5dffa7..4adf8b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -65,3 +65,5 @@ # cmd interpreta mal los .bat/.cmd con LF *.bat text eol=crlf *.cmd text eol=crlf +# git apply necesita los parches en LF, como el codigo de FreeRDP +*.patch text eol=lf diff --git a/Core/Api/StreamSessionService.cs b/Core/Api/StreamSessionService.cs index dd1bc9c..4bc0155 100644 --- a/Core/Api/StreamSessionService.cs +++ b/Core/Api/StreamSessionService.cs @@ -486,14 +486,25 @@ public void ReconnectRdp(Guid id, int width, int height, int frameRate, string? display: DisplayOptions(session)); session.RdpSessionId = rdpId; session.FreeRdpPid = clientPid; - // El audio sale por el cliente RDP del host: el nuevo cliente también se silencia. - if (clientPid > 0) AudioSessionMuter.StartPersistentMute((uint)clientPid); + if (clientPid > 0) MuteRdpClientOnHost(session, clientPid); RdpSessionCreator.ApplyDisplayFrequency(rdpId, frameRate); Logger.Log($"[{session.Name}] Pantalla RDP: {width}x{height} @ {frameRate} fps" + (microphone is null ? "." : $", micrófono '{microphone}'."), id); } /// Usuario de Windows de la sesión (el dedicado si está aislada). + /// + /// Evita que el host oiga el audio de la sesión. mstsc reproduce el audio redirigido, así + /// que se silencia su sesión de audio (por proceso, no toca el volumen global). wfreerdp + /// ya lo descarta con /sound:sys:fake y no necesita nada. + /// + private static void MuteRdpClientOnHost(StreamSession session, int clientPid) + { + if (RdpSessionCreator.IsFreeRdpProcess(clientPid)) return; + Logger.Log($"[{session.Name}] Silenciando el cliente RDP (PID={clientPid}) en el host...", session.Id); + AudioSessionMuter.StartPersistentMute((uint)clientPid); + } + private static StreamUserConfig ResolveUser(StreamSession session) { if (session.Isolated) @@ -942,28 +953,21 @@ private void DoStart(StreamSession session) session.RdpSessionId = rdpId; session.FreeRdpPid = freeRdpPid; - // ── Audio: aislamiento via mute per-process de wfreerdp ─────────── - // FreeRDP /audio-mode:redirect + /sound: RDP server intercepta el audio - // del juego dentro de la sesión RDP y lo redirige por canal a wfreerdp.exe, - // que lo reproduce en el host (típicamente en el endpoint físico). - // Sunshine, dentro de la sesión RDP, hace WASAPI loopback en "Remote Audio" - // (endpoint virtual que crea Windows automáticamente cuando hay redirect), - // capturando el SOURCE del audio antes del transporte RDP. Moonlight oye. - // Para que el host NO oiga: mute persistente de la audio session de - // wfreerdp.exe via ISimpleAudioVolume.SetMute. Mute es per-process por - // endpoint, no toca volumen global ni el de otros procesos. Sunshine no - // se ve afectado porque captura el source, no el output de wfreerdp. + // ── Audio: que el host no oiga la sesión ────────────────────────── + // Con audio redirigido, el servidor RDP saca el audio de la sesión por + // "Remote Audio" (donde Sunshine hace loopback: Moonlight oye) y además lo + // manda por el canal al cliente RDP del host. wfreerdp lo descarta + // (/sound:sys:fake); mstsc lo reproduciría, así que se silencia su sesión + // de audio (ver MuteRdpClientOnHost). if (freeRdpPid > 0) { - Logger.Log($"[{session.Name}] Silenciando wfreerdp.exe (PID={freeRdpPid}) en host para evitar fuga de audio...", session.Id); - AudioSessionMuter.StartPersistentMute((uint)freeRdpPid); + MuteRdpClientOnHost(session, freeRdpPid); } else { - // Sesión RDP reutilizada (no se lanzó FreeRDP). Sin PID de wfreerdp no - // hay nada que silenciar en el host; el audio debería seguir confinado - // al wfreerdp.exe original (si sigue vivo y muteado por su thread). - Logger.Warning($"[{session.Name}] Sin PID de FreeRDP, no se puede aplicar mute en host esta vez.", session.Id); + // Sesión RDP reutilizada: el cliente original sigue conectado y, si era + // mstsc, su hilo de silenciado sigue activo. + Logger.Log($"[{session.Name}] Sesión RDP reutilizada: no se lanzó cliente RDP nuevo.", session.Id); } session.StartPhase = "profile"; @@ -1194,7 +1198,7 @@ private static SunshineConfigurator.SunshineConfig BuildSunshineConfig(StreamSes OutputName: s.OutputName, OriginWebUiAllowed: s.OriginWebUiAllowed, // Sin audio_sink → Sunshine usa el default de la sesión RDP donde corre. - // Con FreeRDP /audio-mode:redirect + /sound, ese default es "Remote Audio" + // Con el audio redirigido (mstsc, o FreeRDP /audio-mode:redirect), es "Remote Audio" // (endpoint que crea Windows para el redirect). Loopback ahí captura el // source del juego. AudioSink: null, diff --git a/Core/Helpers/RdpSessionCreator.cs b/Core/Helpers/RdpSessionCreator.cs index db49d7e..5c1a60c 100644 --- a/Core/Helpers/RdpSessionCreator.cs +++ b/Core/Helpers/RdpSessionCreator.cs @@ -85,6 +85,22 @@ public sealed record RdpSessionInfo( static string FreeRdpExe => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FreeRDP", "wfreerdp.exe"); + /// + /// True si el PID es el wfreerdp incluido. Ese cliente descarta el audio redirigido + /// (/sound:sys:fake), así que, a diferencia de mstsc, no hay que silenciarlo. + /// + public static bool IsFreeRdpProcess(int pid) + { + try + { + using var process = Process.GetProcessById(pid); + return string.Equals(process.ProcessName, Path.GetFileNameWithoutExtension(FreeRdpExe), + StringComparison.OrdinalIgnoreCase); + } + catch (ArgumentException) { return false; } // ya terminó + catch (InvalidOperationException) { return false; } + } + /// /// Loopback alternativo: permite que el RDP local no choque con conexiones /// RDP reales a 127.0.0.1 y que cmdkey guarde credenciales para un target @@ -496,8 +512,11 @@ static uint CreateSessionWithFreeRdp(StreamUserConfig user, string address, int psi.ArgumentList.Add("/network:lan"); // ── Audio ───────────────────────────────────────────────────────────── + // redirect: el audio de la sesión sale por "Remote Audio", donde lo captura Sunshine. + // El backend fake confirma cada bloque al servidor y lo descarta: el host no lo oye + // y no hace falta silenciar wfreerdp (mstsc sí, ver AudioSessionMuter). psi.ArgumentList.Add("/audio-mode:redirect"); - psi.ArgumentList.Add("/sound:sys:winmm"); + psi.ArgumentList.Add("/sound:sys:fake"); if (microphone is not null) psi.ArgumentList.Add($"/microphone:sys:winmm,dev:{microphone}"); diff --git a/FreeRDP/freerdp-proxy.exe b/FreeRDP/freerdp-proxy.exe deleted file mode 100644 index a7423f8..0000000 Binary files a/FreeRDP/freerdp-proxy.exe and /dev/null differ diff --git a/FreeRDP/sdl2-freerdp.exe b/FreeRDP/sdl2-freerdp.exe deleted file mode 100644 index cc1c15c..0000000 Binary files a/FreeRDP/sdl2-freerdp.exe and /dev/null differ diff --git a/FreeRDP/sdl3-freerdp.exe b/FreeRDP/sdl3-freerdp.exe deleted file mode 100644 index 2ea579a..0000000 Binary files a/FreeRDP/sdl3-freerdp.exe and /dev/null differ diff --git a/FreeRDP/sfreerdp-server.exe b/FreeRDP/sfreerdp-server.exe deleted file mode 100644 index 004f00c..0000000 Binary files a/FreeRDP/sfreerdp-server.exe and /dev/null differ diff --git a/FreeRDP/wfreerdp.exe b/FreeRDP/wfreerdp.exe index 40b1381..1815fa3 100644 Binary files a/FreeRDP/wfreerdp.exe and b/FreeRDP/wfreerdp.exe differ diff --git a/FreeRDP/winpr-hash.exe b/FreeRDP/winpr-hash.exe deleted file mode 100644 index e9887a0..0000000 Binary files a/FreeRDP/winpr-hash.exe and /dev/null differ diff --git a/FreeRDP/winpr-makecert.exe b/FreeRDP/winpr-makecert.exe deleted file mode 100644 index 3bc5ad4..0000000 Binary files a/FreeRDP/winpr-makecert.exe and /dev/null differ diff --git a/Services/Session/AudioSessionMuter.cs b/Services/Session/AudioSessionMuter.cs index 8e323f7..0df0e65 100644 --- a/Services/Session/AudioSessionMuter.cs +++ b/Services/Session/AudioSessionMuter.cs @@ -7,11 +7,12 @@ namespace OpenStreamMS.Services.Session; /// endpoint render activo del host. Usa WASAPI IAudioSessionManager2 + /// IAudioSessionControl2.GetProcessId + ISimpleAudioVolume.SetMute. /// -/// Caso de uso: silenciar a wfreerdp.exe en el host. wfreerdp recibe el audio -/// redirigido por RDP y lo reproduce en su endpoint default (físico). Mute de su -/// audio session = host mudo. Sunshine, dentro de la sesión RDP, sigue capturando -/// el audio en el endpoint origen ("Remote Audio") por loopback antes del transporte -/// RDP, así que Moonlight no se ve afectado. +/// Caso de uso: silenciar a mstsc.exe en el host. mstsc recibe el audio redirigido +/// por RDP y lo reproduce en su endpoint default (físico). Mute de su audio session +/// = host mudo. Sunshine, dentro de la sesión RDP, sigue capturando el audio en el +/// endpoint origen ("Remote Audio") por loopback antes del transporte RDP, así que +/// Moonlight no se ve afectado. wfreerdp no lo necesita: va con /sound:sys:fake +/// y descarta el audio sin reproducirlo. /// public static class AudioSessionMuter { diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b999c18..d1f7484 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -9,8 +9,9 @@ remains under its own license; the full license text ships next to the binaries. | Component | Version | License | License file | Source code | |---|---|---|---|---| -| [Sunshine](https://github.com/LizardByte/Sunshine) (LizardByte), [sunshine-webrtc](https://github.com/OpenstreamCo/Sunshine-Web-RTC) build: upstream Sunshine plus Moonlight WebRTC for Samsung TVs | 2026.1006.152353-16-gbcde822b (commit `bcde822bd8f5d750be1eb71c8ccbcb87c98f0b7a`, upstream base `0594f62d4cc6179aa055f0363043adbc8849b62b`) | GPL-3.0 | `Sunshine/LICENSE.txt` | https://github.com/OpenstreamCo/Sunshine-Web-RTC/tree/bcde822bd8f5d750be1eb71c8ccbcb87c98f0b7a | -| [FreeRDP](https://github.com/FreeRDP/FreeRDP) | 3.32.0 | Apache-2.0 | `FreeRDP/LICENSE.txt` | https://github.com/FreeRDP/FreeRDP | +| [Sunshine](https://github.com/LizardByte/Sunshine) (LizardByte), [sunshine-webrtc](https://github.com/mlopezsegura/Sunshine-Web-RTC) build: upstream Sunshine plus Moonlight WebRTC for Samsung TVs | 2026.1006.152353-16-gbcde822b (commit `bcde822bd8f5d750be1eb71c8ccbcb87c98f0b7a`, upstream base `0594f62d4cc6179aa055f0363043adbc8849b62b`) | GPL-3.0 | `Sunshine/LICENSE.txt` | https://github.com/mlopezsegura/Sunshine-Web-RTC/tree/bcde822bd8f5d750be1eb71c8ccbcb87c98f0b7a | +| [FreeRDP](https://github.com/FreeRDP/FreeRDP), [SlimFreeRDP](https://github.com/mlopezsegura/SlimFreeRDP) build: `wfreerdp.exe` only, static, with the patch in `FreeRDP/patches` | 3.33.0 (SlimFreeRDP `v3.33.0-slim.1`) | Apache-2.0 | `FreeRDP/LICENSE.txt` | https://github.com/mlopezsegura/SlimFreeRDP/tree/v3.33.0-slim.1 | +| [OpenSSL](https://www.openssl.org) (statically linked into `wfreerdp.exe`) | 3.6.5 | Apache-2.0 | `FreeRDP/LICENSE.txt` | https://github.com/openssl/openssl/tree/openssl-3.6.5 | | [zlib](https://zlib.net) (bundled with Sunshine) | — | zlib | `Sunshine/zlib-LICENSE.txt` | https://github.com/madler/zlib | Sunshine's web UI (`Sunshine/assets/web`) contains compiled front-end libraries that are diff --git a/docs/puestos-locales.md b/docs/puestos-locales.md index 5dfbf78..6f08c72 100644 --- a/docs/puestos-locales.md +++ b/docs/puestos-locales.md @@ -92,7 +92,7 @@ Sesión del puesto | `Services/Seats/HandleBroker.cs` | Duplica handles NT de texturas compartidas entre procesos de distintas sesiones | | `Services/Seats/DisplayTopology.cs` | Enumerar monitores, activarlos, colocación en esquina y sacarlos del escritorio | | `Services/Seats/SeatAudio.cs` | Salidas de audio y micrófonos disponibles | -| `FreeRDP/patches/0001-audin-winmm-choose-microphone.patch` | Parche de FreeRDP 3.32.0: `/microphone:sys:winmm,dev:` abre ese micrófono | +| `FreeRDP/patches/0001-audin-winmm-choose-microphone.patch` | Parche de FreeRDP 3.33.0: `/microphone:sys:winmm,dev:` abre ese micrófono | | `Services/Seats/SeatUsbCapture.cs` | Cambio de driver a WinUSB y vuelta (con registro en `%ProgramData%\OpenStreamMS\usb-capture.json`) y lectura de los informes USB | | `Services/Seats/SeatHid.cs` | Descriptor de informe HID → eventos de ratón y scancodes de teclado | | `Services/Seats/SeatInput.cs` | Respaldo: Raw Input en la consola y escudo del cursor | @@ -152,8 +152,13 @@ restauran en su siguiente arranque. la directiva «Permitir la redirección de grabación de audio» no esté deshabilitada (`fDisableAudioCapture`). Un micrófono desconectado al encender el puesto se omite (el puesto arranca sin él y queda un aviso en el log). -- **Recompilar FreeRDP**: aplicar el parche sobre FreeRDP 3.32.0 y compilar con el - script estático (`wfreerdp` sin dependencias); copiar `wfreerdp.exe` a `FreeRDP/`. +- **Recompilar FreeRDP**: `wfreerdp.exe` sale de [SlimFreeRDP](https://github.com/mlopezsegura/SlimFreeRDP) + (FreeRDP 3.33.0 + OpenSSL 3.6.5, estático, solo los canales que se usan y con el parche + del micrófono). Compilar con su `build.cmd` y copiar `wfreerdp.exe` a `FreeRDP/`. No trae + `/gfx` ni `/rfx`, ni portapapeles ni unidades. +- **Audio del cliente RDP en el host**: `wfreerdp` va con `/sound:sys:fake` (descarta el + audio redirigido; Sunshine lo captura antes, en «Remote Audio»). mstsc sí lo reproduce y + se silencia su sesión de audio (`AudioSessionMuter`). - **Consola anfitriona**: recomendable un usuario técnico con autologon, sin bloqueo, sin UAC en escritorio seguro y sin apagado de pantalla.