Skip to content

Blog Details

Home/Blog Details

SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
Article cover card for: A call between two browsers, with no media server in the middle
WebRTC

A call between two browsers, with no media server in the middle

Abdulbasit AbdulalimWritten by Abdulbasit Abdulalim
| 5 min Read

The short version

There is a call button on this site. When you press it, your browser and mine talk directly to each other - your voice, my voice, and if one of us shares a screen, the screen. No audio passes through a server, because a server that handles audio is a server that has to be paid for, kept alive, and trusted with every conversation it carries.

What the server does do is much smaller: it tells the two browsers they have a call, it passes the connection details between them, and it keeps a record that the call happened. That is the whole job.

What the server actually does

Three things, and no more:

  • It announces the call. One side presses the button, the server tells the other side a call is arriving.
  • It carries the handshake. The two browsers need to exchange offer, answer and ICE candidates - the technical details of how to find each other and agree on codecs. The server forwards those messages. It cannot read the media because the media never touches it.
  • It remembers that it happened. A row in a table, so a missed call can be noticed and answered later.

Everything else - the call itself, the ringtone, the microphone, the screen share, the file transfer, the chat - happens between the two browsers.

The file transfer is the nicest example of this. Files move over a data channel that belongs to the call itself. There is no upload endpoint, nothing is stored, and when the call ends the channel is gone. A file sent during a call cannot be read by the server, because the server was never in that conversation.

The one-offer rule

Here is the constraint that shaped the entire design, and it is not obvious.

In the normal WebRTC flow, the caller sends an offer and the callee answers. If something changes later - a new video track appears, a screen share starts, a track is added - the browser generates a new offer and sends it, and the other side answers again. This is called renegotiation, and it is completely standard.

The relay in this project does not carry renegotiation. It carries exactly one offer, one answer, and a stream of candidates. That is a deliberate limit: it keeps the server small, and it keeps one side of the connection from being able to hold the call open indefinitely.

The consequence is that the shape of the media must be decided before the handshake, not during it. If a track needs to appear later, it has to be swapped into a slot that was already negotiated.

The video slot

So the call negotiates a video slot up front, even when nobody is using video.

The technique is a transceiver - a slot in the connection that a track can be placed into later. It is negotiated with the intention of sending and receiving video, but it starts empty. When the call starts with audio only, that empty slot is there, waiting.

When the camera is on instead, the camera track occupies the slot directly, and the same trick is not needed - the slot exists because the track exists.

Either way, by the time the call is connected, there is a video slot on each side. And that is the entire reason screen sharing can work at all in this design.

Sharing a screen, without a second offer

Screen sharing is a track swap, not a new stream.

The screen is captured by the browser, which produces a video track like any other. The receiving side already has a video slot. So the sending side takes the screen track and puts it into the existing slot, replacing the camera track:

``js const screen = await navigator.mediaDevices.getDisplayMedia({ video: true }); const sender = findVideoSender(pc); // the slot, not the camera await sender.replaceTrack(screen.getVideoTracks()[0]); ``

replaceTrack does not renegotiate. The connection is not disturbed, the call does not blink, and the other side sees the content change without a single extra message passing through the relay.

The detail that took the longest to get right was finding the slot. A naive search looks for a sender that already has a video track - which works while the camera is on and fails the moment it is off, because an empty transceiver's sender has no track to find. The reliable way is to ask the connection for its transceivers and find the one whose receiver is a video track, because the receiving side of the slot is video even when the sending side is empty.

When the screen share stops, the camera track goes back into the same slot, and the other side sees the camera again. No teardown, no second negotiation, no flicker.

What breaks when you get it wrong

Two failures, both worth naming because both are invisible in a test that only checks "the button exists".

The button that does nothing. An earlier version of the React call surface asked the browser for a screen, stored the stream in state, and stopped there. The stream never reached the peer connection. The button was there, the permission prompt appeared, and the other person never saw a thing. The fix was not a small one - the share has to find the same slot the camera would have used, and the local preview has to show the shared screen rather than the camera.

The slot that does not exist. On the owner side, the empty transceiver was only created when there was no camera. When the owner was on camera, the slot existed - so the share worked. When the owner turned the camera off, the slot disappeared from the sender list, the share failed, and the only symptom was a message saying there was no video slot to share into. The rule that came out of it: always negotiate the slot, and always find it through the transceivers rather than through the tracks.

The rules I keep coming back to

  • Decide the shape of the media before the handshake if the server carries one offer. Everything else follows from that.
  • A capability the user can press and see nothing is worse than a missing one. Either the feature works on every path, or the control is not there.
  • The server should hold as little as possible. A relay that carries only signalling can be small, cheap, and boring - which is exactly what you want from the component that every call depends on.

The same rules are why the files sent during a call are never stored, and why a call that ends leaves nothing behind but a row saying it happened.

Contact Me

Let's Talk for Your
Next Projects

0 / 4000

Or email olaleke2004@gmail.com

Where

Based in Lagos, Nigeria
Working with clients worldwide

Contact

Email : olaleke2004@gmail.com

Time

within 24 hours

Stay Connected

SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers
SaaS Platforms
Marketplaces
Mobile Apps
AI Integration
Admin Dashboards
API Development
Automation
Takeovers