Skip to content
Open
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
4 changes: 4 additions & 0 deletions cmake/compile_definitions/windows.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/mic_write.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/mic_write.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/vibepollo_vmic.h"
"${CMAKE_SOURCE_DIR}/src/platform/windows/vibepollo_vmic.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/windows/virtual_display.h"
${SUNSHINE_WINDOWS_VDISPLAY_SOURCES}
"${CMAKE_SOURCE_DIR}/src/platform/windows/utils.h"
Expand Down
95 changes: 95 additions & 0 deletions docs/mic/uplink-rtp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Client microphone uplink (UDP/RTP)

## Goals

- Real media-plane uplink (not control-stream `0x3003`).
- Compatible wire with Foundation Sunshine clients (`base+12`, RTSP SETUP `mic`).
- Host sink: **Steam Streaming Microphone only** (no VB-Cable auto-install).
- Advertise **Capable / Ready / Port** in `/serverinfo` (not RS-FEC).
- **Opportunistic RS-FEC**: no capability flag; if a client sends parity shards, use them per-session.

## Ports

| Offset | Role |
|--------|------|
| +9 | video |
| +10 | control (ENet) |
| +11 | audio downlink |
| +12 | **mic uplink** (`MIC_STREAM_PORT`) |

## Discovery (`/serverinfo`, HTTPS)

| Field | Meaning |
|-------|---------|
| `MicrophoneCapable` | Feature enabled in config (Windows build with mic uplink). |
| `MicrophoneReady` | Steam Streaming Microphone render endpoint is present **now**. |
| `MicrophonePort` | `net::map_port(12)` (informational; RTSP SETUP is authoritative). |

**Not advertised:** RS-FEC. New clients may send parity without probing a flag.

Legacy Foundation clients ignore unknown nodes.

## RTSP

### DESCRIBE (when `stream_mic` and policy allows)

```
m=audio <mic_port> RTP/AVP 96
a=rtpmap:96 opus/48000/2
a=fmtp:96 minptime=10;useinbandfec=1
```

### SETUP

`SETUP .../mic/...` → `Transport: server_port=<MIC_STREAM_PORT>`, sets session `mic.enabled`.

### Encryption

- Support `SS_ENC_MIC` (0x08) when the client enables it (Foundation-compatible IV scheme).
- Do not require encryption for baseline plaintext Foundation clients.

## Wire

### Baseline (Foundation clients)

1. UDP to mic port.
2. RTP (PT 96/97) or 16-bit extended header type `0x5504`.
3. Sequence number: **little-endian** (Foundation client quirk).
4. Payload: Opus mono @ 48 kHz.

### Opportunistic RS-FEC (new clients)

- Data: same as baseline.
- FEC: RTP `packetType=127` + `AUDIO_FEC_HEADER` + parity (mirror downlink audio layout).
- Geometry: `RTPA_DATA_SHARDS=4`, `RTPA_FEC_SHARDS=2`.
- **Per-session**: first FEC shard seen enables RS assembly for that session only.
- Incomplete blocks fall back to PLC / skip; never break baseline sessions.

## Host path

```
UDP recv → classify data vs FEC
→ (optional SS_ENC_MIC decrypt)
→ (optional RS recover if session saw FEC)
→ Opus decode → float PCM
→ Steam Streaming Microphone (WASAPI render)
→ optional default capture switch to Microphone (Steam Streaming Microphone)
```

## Config (`sunshine.conf`)

| Key | Default | Meaning |
|-----|---------|---------|
| `stream_mic` | `true` | Master enable |
| `mic_require_steam` | `true` | If Ready=0, skip DESCRIBE mic / reject useful uplink |
| `mic_sink` | `Speakers (Steam Streaming Microphone)` | Render endpoint patterns |
| `mic_capture_device` | `Microphone (Steam Streaming Microphone)` | Default capture switch (empty = don't switch) |
| `mic_buffer_ms` | `50` | WASAPI render buffer hint |
| `mic_buffer_packets` | `2` | Jitter prebuffer depth |

## Non-goals

- No VB-Cable download/install.
- No control-stream mic as the primary path.
- No RS-FEC capability advertisement.
- No claiming Opus in-band FEC is transport RS-FEC.
12 changes: 12 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,12 @@ namespace config {
true, // install_steam_drivers
true, // keep_sink_default
true, // auto_capture
true, // stream_mic (Windows uplink; no-op on other platforms without backend)
true, // mic_require_steam
"Speakers (Steam Streaming Microphone)", // mic_sink
"Microphone (Steam Streaming Microphone)", // mic_capture_device
50, // mic_buffer_ms
2, // mic_buffer_packets
};

stream_t stream {
Expand Down Expand Up @@ -2013,6 +2019,12 @@ namespace config {
bool_f(vars, "install_steam_audio_drivers", audio.install_steam_drivers);
bool_f(vars, "keep_sink_default", audio.keep_default);
bool_f(vars, "auto_capture_sink", audio.auto_capture);
bool_f(vars, "stream_mic", audio.stream_mic);
bool_f(vars, "mic_require_steam", audio.mic_require_steam);
string_f(vars, "mic_sink", audio.mic_sink);
string_f(vars, "mic_capture_device", audio.mic_capture_device);
int_between_f(vars, "mic_buffer_ms", audio.mic_buffer_ms, {10, 200});
int_between_f(vars, "mic_buffer_packets", audio.mic_buffer_packets, {1, 16});

string_restricted_f(vars, "origin_web_ui_allowed", nvhttp.origin_web_ui_allowed, {"pc"sv, "lan"sv, "wan"sv});
// reflect origin ACL update immediately in HTTP layer
Expand Down
8 changes: 8 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ namespace config {
bool install_steam_drivers;
bool keep_default;
bool auto_capture;

// Client microphone uplink (UDP/RTP → Steam Streaming Microphone)
bool stream_mic; ///< Advertise/accept mic uplink when capable
bool mic_require_steam; ///< If true, DESCRIBE/SETUP mic only when Steam mic is present
std::string mic_sink; ///< WASAPI render endpoint (Steam Streaming Microphone speakers side)
std::string mic_capture_device; ///< Optional default capture device to select for host apps
int mic_buffer_ms; ///< WASAPI render buffer hint (10-200)
int mic_buffer_packets; ///< Jitter prebuffer depth in Opus packets (1-16)
};

constexpr int ENCRYPTION_MODE_NEVER = 0; // Never use video encryption, even if the client supports it
Expand Down
11 changes: 11 additions & 0 deletions src/nvhttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,17 @@ namespace nvhttp {
tree.put("root.VirtualDisplayDriverReady", true);
}
#endif
// Client microphone uplink advertisement and formal protocol discovery.
{
auto mic = stream::get_mic_status();
tree.put("root.MicrophoneCapable", mic.capable);
tree.put("root.MicrophoneReady", mic.ready);
if (mic.port) {
tree.put("root.MicrophonePort", mic.port);
}
tree.put("root.MicrophoneProtocol", "moonlight-mic");
tree.put("root.MicrophoneProtocolVersions", "1");
}
} else {
tree.put("root.mac", "00:00:00:00:00:00");
tree.put("root.Permission", "0");
Expand Down
53 changes: 53 additions & 0 deletions src/platform/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,15 @@ namespace platf {
virtual ~mic_t() = default;
};

/**
* @brief Saved default capture endpoints so mic uplink can restore them later.
*/
struct capture_snapshot_t {
std::string console_id;
std::string comms_id;
std::string multimedia_id;
};

class audio_control_t {
public:
virtual int set_sink(const std::string &sink) = 0;
Expand All @@ -639,6 +648,45 @@ namespace platf {
*/
virtual void reset_default_device(const std::string &preferred_device = {}) {}

/**
* @brief Initialize Steam Streaming Microphone render backend for client mic uplink.
* @return 0 on success, -1 if unavailable (Steam not running / endpoint missing).
*/
virtual int init_mic_redirect_device() {
return -1;
}

/**
* @brief Release the mic redirect backend.
*/
virtual void release_mic_redirect_device() {}

/**
* @brief Write mono float32 PCM into the mic redirect backend.
*/
virtual int write_mic_pcm(const float * /*samples*/, std::uint32_t /*count*/) {
return -1;
}

/**
* @brief True if the Steam mic render endpoint can be opened right now.
*/
virtual bool mic_redirect_available() {
return false;
}

virtual capture_snapshot_t snapshot_capture_defaults() {
return {};
}

virtual void switch_capture_to(const std::string & /*device_name*/) {}

virtual void restore_capture_from(const capture_snapshot_t & /*snapshot*/) {}

virtual std::string get_current_default_capture_name() {
return {};
}

virtual ~audio_control_t() = default;
};

Expand All @@ -657,6 +705,11 @@ namespace platf {

std::unique_ptr<audio_control_t> audio_control();

/**
* @brief Check for an active microphone redirect endpoint without initializing it.
*/
bool mic_redirect_available();

/**
* @brief Get the display_t instance for the given hwdevice_type.
* If display_name is empty, use the first monitor that's compatible you can find
Expand Down
Loading
Loading