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

@@ -46,6 +46,9 @@ internal struct VcStreamDescNative
public IntPtr DeviceId; // unused by vc_stream_start today — device selection is a
// separate vc_set_input_device call; always IntPtr.Zero here.
public IntPtr Label;
// Mirrors vc_stream_desc::external_feed. When 1, the core skips its own WASAPI loopback
// and the caller feeds PCM via StreamFeedPcm (per-app capture path on Windows).
public int ExternalFeed;
}
[StructLayout(LayoutKind.Sequential)]

View File

@@ -186,6 +186,26 @@ public sealed class VoiceCatClient : IDisposable
}
}
/// <summary>
/// Like <see cref="StartStream"/> but sets <c>external_feed = 1</c> so the core skips its
/// own WASAPI loopback. The caller is responsible for feeding PCM via
/// <see cref="StreamFeedPcm"/>. Used by the Windows per-app capture path.
/// </summary>
public (VcResult Result, uint StreamId) StartStreamExternalFeed(VcStreamKind kind, string label)
{
nint labelPtr = Marshal.StringToCoTaskMemUTF8(label);
try
{
var desc = new VcStreamDescNative { Kind = kind, DeviceId = 0, Label = labelPtr, ExternalFeed = 1 };
var r = NativeMethods.vc_stream_start(_handle.DangerousGetHandle(), in desc, out uint streamId);
return (r, streamId);
}
finally
{
Marshal.FreeCoTaskMem(labelPtr);
}
}
public VcResult StopStream(uint streamId) =>
NativeMethods.vc_stream_stop(_handle.DangerousGetHandle(), streamId);