Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 24 additions & 20 deletions Core/Api/StreamSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>Usuario de Windows de la sesión (el dedicado si está aislada).</summary>
/// <summary>
/// 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 <c>/sound:sys:fake</c> y no necesita nada.
/// </summary>
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)
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 20 additions & 1 deletion Core/Helpers/RdpSessionCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ public sealed record RdpSessionInfo(
static string FreeRdpExe =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FreeRDP", "wfreerdp.exe");

/// <summary>
/// True si el PID es el wfreerdp incluido. Ese cliente descarta el audio redirigido
/// (<c>/sound:sys:fake</c>), así que, a diferencia de mstsc, no hay que silenciarlo.
/// </summary>
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; }
}

/// <summary>
/// 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
Expand Down Expand Up @@ -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}");

Expand Down
Binary file removed FreeRDP/freerdp-proxy.exe
Binary file not shown.
Binary file removed FreeRDP/sdl2-freerdp.exe
Binary file not shown.
Binary file removed FreeRDP/sdl3-freerdp.exe
Binary file not shown.
Binary file removed FreeRDP/sfreerdp-server.exe
Binary file not shown.
Binary file modified FreeRDP/wfreerdp.exe
Binary file not shown.
Binary file removed FreeRDP/winpr-hash.exe
Binary file not shown.
Binary file removed FreeRDP/winpr-makecert.exe
Binary file not shown.
11 changes: 6 additions & 5 deletions Services/Session/AudioSessionMuter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ namespace OpenStreamMS.Services.Session;
/// endpoint render activo del host. Usa WASAPI <c>IAudioSessionManager2</c> +
/// <c>IAudioSessionControl2.GetProcessId</c> + <c>ISimpleAudioVolume.SetMute</c>.
///
/// 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 <c>/sound:sys:fake</c>
/// y descarta el audio sin reproducirlo.
/// </summary>
public static class AudioSessionMuter
{
Expand Down
5 changes: 3 additions & 2 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions docs/puestos-locales.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<id de endpoint>` abre ese micrófono |
| `FreeRDP/patches/0001-audin-winmm-choose-microphone.patch` | Parche de FreeRDP 3.33.0: `/microphone:sys:winmm,dev:<id de endpoint>` 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 |
Expand Down Expand Up @@ -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.

Expand Down
Loading