Soundsync

Soundsync

WebRTCWebAudioWASMN-API

Soundsync is an open-souce web and native app to manage real-time audio streams between multiple devices on the same local network. It groups different devices and their audio output together to broadcast the same audio stream in sync. It's built with NodeJS and React.

This is the most technically complex project I built. It required solving a lot of new challenges in networking, synchronization, data handling, cross-OS, cross-browser compatibility, and more. It's now used by a few hundred people every day, including me.

Here are some interesting problems I needed to solve to make Soundsync work:

WebRTC discoverability

There's no concept of server/client in Soundsync. Every Soundsync install connects to every other peer in the network with a WebRTC connection. DataChannels are used to send commands between clients and send/receive audio data. Even the React WebUI is a client in the network and uses the same code to discover, connect to and communicate with other peers.

WebRTC is well adapted for this usage because it doesn't require clients to bind to a port. Using multiple DataChannels is also very useful to manage multiple audio streams in parallel.

Discoverability on the local network is very tricky because there's no central server. Multiple NodeJS processes can use UDP broadcast (or Bonjour in this case) to discover each other. I wanted to optimize the UX as much as possible and so reduce manual configuration to the minimum. This means letting the user open Soundsync on their browser and controlling each peer directly without any manual pairing. There's no native browser API to work with UDP broadcast.

To fix the WebUI discoverability problem and support networks where UDP broadcast is blocked, I built a simple external registry service. I host this service on one of my servers. It allows peers to register their internal IP address and query all other addresses on the same external IP. The WebUI client then queries this HTTPS endpoint to get the addresses of all Soundsync instances on the local network. Of course, this works only for simple networks with a single external IP address.

Connecting the web controller with a local peer

Solving the connection problem between the WebUI and a local peer was more complex. I needed to prevent external devices from joining the local Soundsync network. I didn't want to rely solely on the external IP to let peers communicate their WebRTC handshake together. The external IP is not sufficient to identify local networks because Some ISP shares the same external IP between multiple clients. I only wanted them to connect to each other if they could communicate through the internal IP.

Initiating the WebRTC connection is not too hard to do. I implemented a rendezvous service that acts as a "mailbox" between two peers. It allows them to send and receive short messages (their WebRTC handshake) by using a single pre-shared UUID (the mailbox ID). Sharing this UUID is the tricky part.

Because Soundsync WebUI is hosted on an HTTPS origin, it's not possible to initiate an HTTP request to a host on the local network (which cannot have a valid SSL certificate). Hosting Soundsync controller with HTTP is also not possible because some Web API I'm using (AudioWorklet) are not available in a non-secure context.

To solve this problem, I implemented a hack around SSL SNI.

When initiating a request to a HTTPS origin, during the SSL handshake, the client sends the unencrypted hostname to the server. This is used to let the server know which certificate to send back. In our case, we don't have a certificate, but we can use SNI to send the mailbox UUID chosen by the browser to the peer. The peer initiates a request to MAILBOX-UUID.ip-192-168-1-10-sslip.io, which is translated to 192.168.1.10 by sslip.io. The HTTPS connection will fail but the other NodeJS peer will receive the UUID in the domain name. This will then be used to initiate the handshake through the mailbox.