feat(windows): per-app screen-audio sharing (include/exclude)
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Adds "only selected apps" and "all apps except selected" screen-audio
modes to the Windows client, alongside the existing entire-desktop path.

Per-app capture uses WASAPI process loopback (AUDCLNT_ACTIVATIONTYPE_
PROCESS_LOOPBACK) via ProcessLoopbackCapture, mixed by ProcessAudioMixer
and fed to the core through vc_stream_feed_pcm (external_feed=1 so the
core skips its own loopback device).

Init must pass AUDCLNT_STREAMFLAGS_LOOPBACK | EVENTCALLBACK |
AUTOCONVERTPCM; the LOOPBACK flag is what makes the virtual endpoint
deliver rendered audio (without it every buffer is flagged SILENT) and
AUTOCONVERTPCM resamples the app's native format to 48k s16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 00:19:38 +02:00
parent 4f89d2d32d
commit e806b698ec
11 changed files with 1064 additions and 24 deletions

View File

@@ -1160,8 +1160,9 @@ vc_result vc_client::stream_start(const vc_stream_desc& desc, uint32_t* out_stre
if (ls.active.load(std::memory_order_acquire) || ls.pending) return VC_ERR_ALREADY;
uint32_t sid = next_local_stream_id_++;
ls.stream_id = sid;
ls.pending = true;
ls.stream_id = sid;
ls.pending = true;
ls.external_feed = (desc.external_feed != 0);
if (out_stream_id) *out_stream_id = sid;
req_id = next_req_id_++;
@@ -1196,6 +1197,7 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
bool ok_to_emit = false;
int kind;
int loopback_channels = 1; // only meaningful for SCREEN_AUDIO; set under the lock
bool loopback_external = false;
{
std::lock_guard lk(local_streams_mu_);
auto pit = pending_announce_kind_.find(req_id);
@@ -1239,12 +1241,13 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
// when the channel is stereo (real L/R, no downmix), mono otherwise. Captured under
// the lock alongside the rest of the LocalStream setup; used below after unlock.
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
loopback_channels = ls.effective_params.stereo ? 2 : 1;
loopback_channels = ls.effective_params.stereo ? 2 : 1;
loopback_external = ls.external_feed;
}
}
ensure_audio_running();
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
if (kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO) && !loopback_external) {
audio_engine_.start_loopback_capture(kind, loopback_channels);
}
@@ -1260,7 +1263,8 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
vc_result vc_client::stream_stop(uint32_t stream_id) {
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
int stopped_kind = -1;
int stopped_kind = -1;
bool stopped_external = false;
{
std::lock_guard lk(local_streams_mu_);
for (auto& [k, ls] : local_streams_) {
@@ -1268,11 +1272,12 @@ vc_result vc_client::stream_stop(uint32_t stream_id) {
}
LocalStream* ls = find_local_stream_by_id(stream_id);
if (!ls || !ls->active.load(std::memory_order_acquire)) return VC_ERR_INVALID_ARG;
stopped_external = ls->external_feed;
ls->active.store(false, std::memory_order_release);
ls->encoder.destroy();
}
if (stopped_kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO)) {
if (stopped_kind == static_cast<int>(VC_STREAM_SCREEN_AUDIO) && !stopped_external) {
audio_engine_.stop_loopback_capture();
}

View File

@@ -246,6 +246,11 @@ struct vc_client {
// VC_STREAM_MIC. Set via vc_set_capture_channels; read by ensure_audio_running() to
// configure AudioParams.capture_channels before the device opens. Defaults to 1 (mono).
uint32_t capture_channels = 1;
// If true, the caller is feeding PCM via vc_stream_feed_pcm — skip start/stop of the
// core's WASAPI loopback device. Set from vc_stream_desc::external_feed at stream_start
// time and checked in handle_stream_announce_result / stream_stop.
bool external_feed = false;
};
mutable std::mutex local_streams_mu_;
std::unordered_map<int, LocalStream> local_streams_; // keyed by vc_stream_kind