Skip to content

Bindings

Melih Ercan edited this page Sep 16, 2026 · 1 revision

Bindings

Five projects that turn each platform's native WebRTC into C#. They are the only layer still split across several assemblies, because each binds something genuinely different — and one of them is not a binding at all.

You never reference a binding. They are compiled into the WebRTCme package with PrivateAssets="all", which keeps them out of your dependency list and out of your code. This page is for people changing them, or diagnosing something that went wrong beneath the API.

Project Binds Native payload Technique
WebRTCme.Bindings.Blazor the browser's WebRTC none JSInterop
Maui/…Maui.Android Google's Java SDK Jars/libwebrtc.aar Java binding (AndroidLibrary)
Maui/…Maui.iOS Google's ObjC SDK WebRTC.xcframework ObjC binding (Objective Sharpie)
Maui/…Maui.MacCatalyst Google's ObjC SDK WebRTC.framework ObjC binding (Objective Sharpie)
Maui/…Maui.Windows a C ABI over libwebrtc native/win-x64/*.dll P/Invoke

Everything except Blazor gets its native half from WebRTCnative. All of them are committed to this repository, so an ordinary dotnet build needs no downloads.

A binding is not the API

Worth being precise, because the layering only makes sense once this is clear. A binding exposes the native SDK's own shape in C# — org.webrtc.PeerConnection as Java declared it, RTCPeerConnection as Objective-C declared it. It does not implement the common API and knows nothing about it.

The mapping onto the common surface happens one layer up, in WebRTCme/Platforms/<Platform>/. See The unified API.

Blazor — the one with no native code

The web already has WebRTC, so there is nothing to bind: the "binding" is a JSInterop wrapper that holds JavaScript object references and calls methods on them.

flowchart LR
  cs["WebRTCme/Platforms/Blazor<br/>RTCPeerConnection.cs"]
  ext["JsRuntimeExtensions<br/>CallJs · GetJsPropertyValue · AddEventListener"]
  ref["JsObjectRef<br/>a handle to a JS object"]
  js["wwwroot/JsInterop.js<br/>served as _content/WebRTCme/JsInterop.js"]
  api["the browser's RTCPeerConnection"]

  cs --> ext --> ref
  ext --> js --> api
Loading

Three things about it are worth knowing:

The JavaScript ships from WebRTCme, not from this project. Only a Razor SDK project can pack static web assets, and WebRTCme.csproj is the Razor SDK project, so wwwroot/JsInterop.js lives there. That is also why the Blazor binding needs no NuGet package of its own. Consumers load it from _content/WebRTCme/JsInterop.js — see Getting started.

A JS return value is a reference, not its contents. JsObjectRef is a handle; reading a property means another interop call. Treating a returned object as though it had already been marshalled is a mistake this codebase has made more than once.

Arguments are reshaped here, in JsRuntimeExtensions. The API models optional dictionary members as nullable properties, and JSInterop serialises a null property as an explicit null rather than omitting it — which the browser rejects, because null is not a member of an enum:

TypeError: Failed to construct 'RTCPeerConnection': Failed to read the 'bundlePolicy' property
from 'RTCConfiguration': The provided value 'null' is not a valid enum value of type
RTCBundlePolicy.

Until 26.9.15 every Blazor consumer had to reach into JSRuntime's non-public JsonSerializerOptions by reflection to fix that, and nothing said so anywhere a consumer would look. The binding now serialises its own model types through a JsonElement with WhenWritingNull, so the host's settings do not matter. If you find yourself needing that reflection workaround again, this has regressed — please report it.

Android — a Java binding

libwebrtc.aar carries classes.jar and four libjingle_peerconnection_so.so, one per ABI. The project is a net10.0-android library with the .aar as an AndroidLibrary, so the Android SDK generates C# for the Java classes.

  • Transforms/Metadata.xml, EnumFields.xml, EnumMethods.xml steer that generation — renaming, hiding and un-mangling what the generator produces from Java.
  • Additions/ is where hand-written C# is added to a generated class, if it ever needs to be.

Java bindings are generous: almost everything public in the SDK arrives, so gaps at the API level are usually unmapped rather than unbound.

One packaging detail that cost real time: the Android SDK builds a second .aar for the binding project itself, which re-emits the same four .so and nothing else — 23 MB of duplication, and two .aar carrying the same .so in one lib/ folder. WebRTCme.csproj removes it after _IncludeAarInNuGetPackage, because the SDK offers no property to say no.

iOS and Mac Catalyst — Objective-C bindings

Two projects, the same Objective-C SDK, two different packagings of it:

  • iOS takes WebRTC.xcframework, which is flat and builds correctly on Windows.
  • Mac Catalyst takes WebRTC.framework, which must be a versioned bundleVersions/A plus symlinks — or macOS refuses to codesign an app embedding it. Git on Windows cannot check those symlinks out without core.symlinks, so for a long time the framework's binary arrived as a 23-byte text file holding a path, and that is what shipped. It is stored flat here and reassembled by build-versioned-framework.sh, and CI builds that slice on a Mac.

ApiDefinitions.cs and StructsAndEnums.cs in each project are the binding definitions, generated by Objective Sharpie from the framework's headers and then edited. Regenerating them after a native refresh is described in WebRTCme.Bindings/README_BuildBindings.txt; the headers need <WebRTC/xxx.h> rewritten to "xxx.h" before Sharpie will read them.

IOS_BINDINGS_BUILD_PROBLEMS_FROM_WINDOWS.txt records what goes wrong doing this from a PC. Read it before assuming a failure is yours.

Both slices compile on Windows. Only linking and deploying an app needs a Mac.

Windows — not a binding at all

There is no Google-published Windows SDK to bind. WebRTCnative builds one instead: WebRtcInterop.dll exposes a flat C ABI, and this project is a single 576-line Interop.cs of P/Invoke declarations.

flowchart LR
  plat["WebRTCme/Platforms/Windows<br/>lifetime, threading, events"]
  interop["Interop.cs<br/>DllImport, one per header line"]
  dll["WebRtcInterop.dll"]
  lib["libwebrtc<br/>absorbed into the shim"]

  plat --> interop --> dll --> lib
Loading

Interop.cs is deliberately mechanical: every entry corresponds to a line in WebRtcInterop/include/Interop.h and nothing in it interprets, wraps or improves on anything. Keeping it dumb is what makes it reviewable against the header. Lifetime, threading and turning callbacks into events all happen a layer up.

Three rules govern everything above it, and breaking any of them produces a leak or a deadlock rather than an exception:

  • A handle arriving through an out-parameter or a callback is owned by the receiver and needs exactly one matching release.
  • A string arriving as a callback argument is borrowed for the duration of the call; a string returned through an out-parameter must be freed with StringFree.
  • No callback may call back in synchronously. Callbacks run on WebRTC's signalling thread, which every peer connection in the process shares, and the interop calls block on it. Feeding a candidate raised by one connection straight into another deadlocks that thread against itself. Copy, queue, and let an ordinary thread make the call.

The full contract is the Interop ABI page of the WebRTCnative wiki.

The native payload is a component build. native/win-x64/ holds WebRtcInterop.dll plus its import closure — abseil, BoringSSL, protobuf, libc++ — and four Microsoft C++ runtime DLLs so the binding works on a machine without the Visual C++ redistributable installed. That last group is a deployment choice rather than a fact, and the folder's own README.md says so. webrtc.dll is not used: the shim absorbs the WebRTC code it needs.

They land in the package at runtimes/win-x64/native/, not in lib/, because they are plain DLLs rather than managed assemblies.

Building them

Everything builds with plain dotnet build on Windows, including the iOS and Mac Catalyst slices:

dotnet build WebRTCme.sln

CI does exactly this on windows-latest, which is the only runner that can build all five target frameworks in one job — net10.0-windows10.0.22621.0 builds nowhere else. The Apple package slices are built on a Mac and merged in, for the codesigning reason above.

Before changing a binding project, read the three notes beside them, each of which records a failure that does not name its own cause:

  • WebRTCme.Bindings/README_BuildBindings.txt — building WebRTC by hand, and regenerating Sharpie definitions. Largely historical now that WebRTCnative does the building.
  • WebRTCme.Bindings/README_NativeSdkVersions.txt — where each artifact came from.
  • WebRTCme.Bindings/IOS_BINDINGS_BUILD_PROBLEMS_FROM_WINDOWS.txt — Apple bindings from a PC.

How complete are they?

Less relevant than it sounds. A binding surface is only interesting where the API above it reaches for something and finds nothing — an unbound method that nothing calls costs nobody anything. What is actually unreachable per platform is in What works where, and the engineering record is doc/KnownGaps.md.

Next layer up: The unified API.

Clone this wiki locally