Windows
A WinUI 3 app on .NET 10, unpackaged and self-contained, for x64 and ARM64. WebRTC is the prebuilt webrtc-sdk/libwebrtc release m150.7871.03, the same branch as the Android and Apple SDKs, reached through a small C shim.
File: docs/public/media/windows-call.pngcd windows
./native/RtcShim/scripts/build-shim.ps1 # downloads libwebrtc, builds rtc_shim.dll
dotnet run --project src/WebRtcDemo.App -p:Platform=x64Or open WebRtcDemo.slnx in Visual Studio 2026 and run x64 or ARM64. Tests, publishing and troubleshooting are in windows/README.md.
Layers
| Project | What it does |
|---|---|
native/RtcShim | The C shim. rtc_shim.h is the whole API. |
WebRtcDemo.Interop | LibraryImport bindings, one SafeHandle per native object, native callbacks |
WebRtcDemo.Core | Signaling, the call state machine, media, group engine. No UI, unit tested on any OS. |
WebRtcDemo.Effects | Backgrounds and stickers: ONNX models, compositing. No UI. |
WebRtcDemo.App | The WinUI 3 app, with the UI built in C# rather than XAML pages |
Why a C shim
libwebrtc's API is C++ classes: virtual methods, scoped_refptr, observer interfaces. C# can't call that directly. The shim, built with MSVC and the static CRT to match libwebrtc.dll, exports plain C instead:
- opaque handles with explicit release, UTF-8 strings, and callbacks with a
void* userargument; - async calls (create offer, set description, stats) that complete through callbacks;
- a closed peer connection that rejects calls instead of crashing;
- video sinks that hand out BGRA, already rotated and optionally downscaled.
On the C# side, callbacks are UnmanagedCallersOnly functions that look their target up by ID, so nothing is pinned and no exception unwinds into native code. If you want to bring libwebrtc to another language, this header is a good map of what a call actually needs.
How it does each part
- Signaling. One
ClientWebSocketper call,SignalingClient.cs. A closed socket ends the call, as on the other clients. - Call state.
CallViewModel.csfollows the same flow as the web and iOS clients: the peer in the room offers, the key goes first, the 300 ms rule. - Switching sources.
NativeCallMedia.csswaps the video sender's track between camera, screen or window, file and effects, which keeps the sender's cryptor. Switching video sources - Camera. The shim reads cameras through Media Foundation, like the Windows Camera app, and tries the formats closest to 1280×720 at 30 fps until one sends frames within 4 s. Cameras Media Foundation can't read go through libwebrtc's DirectShow capturer.
- Effects. MediaPipe models converted to ONNX, run by ONNX Runtime through Windows ML on the GPU or NPU, with compositing in C#. Backgrounds and effects
- Picture-in-picture. The
CompactOverlaypresenter. Picture-in-picture
Two Windows-specific problems worth knowing
The microphone. libwebrtc's Windows audio device hands echo cancellation to Windows' voice-capture DMO, which only captures while playout runs. A call usually starts sending before any audio arrives, and a group call's publish connection never plays anything, so recording failed and was never retried: the others heard nothing. The shim keeps the DMO out of the audio device, so capture is plain WASAPI, echo-cancelled by libwebrtc's own AEC3.
Rounded video. A SwapChainPanel is "external content": Windows draws it below WinUI's own rendering, through a hole, so no rounded clip reaches it. Each video view is instead a composition sprite whose brush shows a drawing surface the size of the frame (ICompositorInterop::CreateGraphicsDevice on a D3D11 device). The compositor draws that surface itself, so the view can clip it with rounded corners. Frames from WebRTC threads go into a one-slot mailbox that the UI thread draws on the next composition tick.
Testing without Windows
windows/scripts/verify.sh builds the shim against the Linux libwebrtc release, runs its loopback test (two encrypted peers in one process), runs all the .NET tests against it, and compiles the app for x64 and ARM64. Linux needs an audio server, since libwebrtc aborts without one. PulseAudio with a null sink is enough.
