WebRTC lets two endpoints talk peer-to-peer over UDP with DTLS encryption and SCTP data channels. Before they can talk, each endpoint has to exchange a small piece of setup information (an SDP offer/answer plus ICE candidates) with the other. WebRTC does not specify how that exchange happens. You bring your own signaling.
SpawnDev.RTC.Signaling is a signaling implementation. It speaks the WebTorrent tracker wire protocol - because that protocol is already a generic room-based WebRTC signaling relay hiding under a torrent-flavored name, and because speaking it means you automatically interop with the public WebTorrent tracker fleet.
SpawnDev.RTC.Signaling(ships inSpawnDev.RTCpackage): the client. Consumers use this to connect to a tracker, join rooms, and exchange offers/answers.SpawnDev.RTC.Server(ships inSpawnDev.RTC.Serverpackage): the server. Host it in any ASP.NET Core app with a one-line extension method (app.UseRtcSignaling("/announce")), or run the standaloneSpawnDev.RTC.ServerAppexe / Docker image.
Both sides speak the same wire format. A SpawnDev.RTC.Signaling.TrackerSignalingClient can connect to wss://tracker.openwebtorrent.com/announce and meet other WebTorrent peers. A stock JS WebTorrent client can connect to your SpawnDev.RTC.ServerApp and torrent. The protocol is symmetric.
A 20-byte opaque value that identifies a room. On the wire it matches the WebTorrent info_hash byte format exactly. Construct from:
var a = RoomKey.FromString("my-lobby-42"); // SHA-1 of UTF-8, no normalization
var b = RoomKey.FromHex("9fa3...20-byte-hex"); // explicit hex
var c = RoomKey.FromBytes(20-byte-array); // torrent info_hash literal
var d = RoomKey.Random(); // app-generated unique roomFromString hashes the literal UTF-8 bytes with SHA-1. No trim, no lowercase, no normalization. A consumer that changes "my-lobby" to "My-Lobby" or " my-lobby " before hashing silently lands in a different room. Be consistent.
The wire-level client. One instance per (announceUrl, peerId) pair - multiple rooms can multiplex on a single connection.
var peerId = new byte[20];
System.Security.Cryptography.RandomNumberGenerator.Fill(peerId);
await using var client = new TrackerSignalingClient(
"wss://tracker.openwebtorrent.com/announce",
peerId);
client.OnConnected += () => Console.WriteLine("connected");
client.OnWarning += w => Console.WriteLine($"warning: {w}");The client by itself doesn't know about IRTCPeerConnection. It just shuttles bytes. To actually create peer connections, pair it with a handler.
Default room handler. Given an ICE configuration, it owns a pool of IRTCPeerConnection instances - one per remote peer - and wires them up to the signaling client.
var handler = new RtcPeerConnectionRoomHandler(new RTCPeerConnectionConfig
{
IceServers = new[]
{
new RTCIceServerConfig { Urls = new[] { "stun:stun.l.google.com:19302" } }
}
});
handler.OnPeerConnection += (pc, peerId) => { /* pc is ready; add tracks / transceivers */ };
handler.OnDataChannel += (channel, peerId) => { /* browser peer opened a DC */ };
handler.OnPeerConnectionCreated += (pc, peerId) => { /* pre-SDP hook for media wiring */ };
var room = RoomKey.FromString("my-lobby");
client.Subscribe(room, handler);
await client.AnnounceAsync(room, new AnnounceOptions { Event = "started", NumWant = 5 });That's the whole end-to-end client-side flow. Anything more specific - custom peer selection, custom data-channel lifecycle, media-only peers - is a matter of writing your own ISignalingRoomHandler and subscribing it the same way.
It's the only widely-deployed WebRTC signaling protocol with a public tracker fleet. Every other option is proprietary (PeerJS server, matrix.org, custom per-app) or rolls its own (most WebRTC tutorials). By speaking the WebTorrent wire, consumers of SpawnDev.RTC.Signaling:
- Get the public fleet for free -
tracker.openwebtorrent.comet al work out of the box with no account, no API key, no rate limit beyond the tracker's own norms. - Can run their own tracker with one command (see run-a-tracker.md) and that tracker simultaneously serves the public WebTorrent network. Running your own infra strengthens the public commons.
- Interop across ecosystems - a WebTorrent client and a SpawnDev.RTC consumer can share a room if they use the same
RoomKeybytes.
The tradeoff: the wire format dictates a 20-byte room identifier. If your app has user-friendly names like "lobby-42", you hash them into RoomKey.FromString(...). There is no registration, no namespace, and no verification. Collisions are cosmic-ray-unlikely with SHA-1 and not a security concern - rooms aren't a trust boundary. For that, use RoomKey.Random() and pass the handle through your own auth'd channel.
- Authentication / access control: rooms are public to anyone who knows the key. If you need auth, do it above this layer (e.g. sign the room key, include a signed handshake in the first data channel message).
- Presence / member list: the tracker's announce response includes a peer list, but that list is a snapshot, not a subscription. For real-time presence, maintain it over the data channel once connected.
- NAT traversal fallback: STUN works through most NATs. For TURN fallback when STUN fails, add TURN URLs to your
RTCIceServerConfig.SpawnDev.RTC.Signalingdoesn't change how WebRTC does NAT traversal - it just delivers offers/answers.
- run-a-tracker.md - host your own signaling server
- use-cases.md - concrete example architectures
SpawnDev.RTC.Demo/Pages/ChatRoom.razor- working Blazor WASM consumerSpawnDev.RTC.DemoConsole/ChatMode.cs- desktop console consumerSpawnDev.RTC.WpfDemo/MainWindow.xaml.cs- desktop WPF consumer with media