Introduction
I've spent way too much time trying to come up with some generic introduction for this story, but then I realized none of you probably want to read that anyway. So instead, I'll just jump straight into the story—which is why you're here in the first place.
The day I received the requirements
The story begins when I received the requirements for a new feature that allows Teachers to share their presentation to review slides before the Lecture begins, so we would have teachers aids using the web version and seeing a screenshare from the main pc powerpoint, at first I thought maybe we can use HLS or RTMP for this and be okay with the 3 seconds delay that it has, but then I continued reading the ticket, we also needed the user to move to the next and previous slides via the web application, which immediately threw my initial idea out of the window. This is because if the user needs to interact with the application there is no way it will be usable without almost immediate feedback.
Since we needed to show this to the client quickly we had 2 weeks to implement this feature, so before I did anything, I stopped and started drafting a simple design doc, which besides the fancy name was really just a document with my raw notes taken from research and comparisons between different solutions.
After spending some time doing research and looking into different architectures and engineering blogs from companies like Twitch, Slack and Discord, I narrowed the possibilities down to four common architectures used for this type of use case.
Architecture Options
P2P Mesh
This approach revolved around a user establishing WebRTC connections with every other user in the room. Besides being difficult to manage in terms of connections and sessions, it had one fatal flaw: network and CPU overhead.
If we had twenty users in the room, every participant would maintain nineteen separate peer connections while sending nineteen streams, quickly consuming both CPU and bandwidth.
MCU (Multipoint Control Unit)
Going back into server-client architectures, an MCU attempts to centralize media by ingesting streams from producers, multiplexing them together, and then sending the processed result to consumers.
This makes connection management significantly easier since every client only sends and receives a single stream.
The downside is that multiplexing introduces additional latency while also making the server responsible for expensive media processing. Since our entire requirement revolved around low latency, this wasn't the direction we wanted to take.
SFU (Selective Forwarding Unit)
This was the most promising option.
Like an MCU, clients only connect to a central server, but instead of processing the media, the server simply forwards streams from producers to consumers.
That means:
- much lower latency,
- significantly lower server CPU usage,
- far better scalability than a mesh network.
Given our requirements, this immediately became the strongest candidate.
Hybrid (Mesh + SFU)
Another common approach is dynamically switching between Mesh and SFU depending on participant count or network conditions.
In theory this offers the best of both worlds.
In practice, it also means maintaining two completely different implementations.
Given our timeline, this was quickly ruled out.
Custom WebRTC implementation
Another approach worth mentioning is modifying WebRTC itself to fit the use case.
Discord, for example, has invested heavily in custom WebRTC optimizations, but doing something similar requires an entire engineering team working at a much lower level than what was realistic for our project.
While not realistic for most teams , it can be still a fun read discord
The MediaSoup Journey
After sending over the architecture document and discussing the trade-offs, we settled on the SFU approach.
The next question became:
How do we actually build one?
We weren't going to implement an SFU from scratch, so I started looking for existing libraries.
The first thing that caught my attention was MediaSoup.
It was extremely popular, battle-tested, and had a very well-designed architecture, so naturally it became the first solution I experimented with.
The Express wrapper and why
So after reading the docs thoroughly and understanding the all the components well I realized something very important , media soup has some class clients that we need to track their state in memory , and as we all know State is evil, so there was no way I add this to the core api server , so I decided to create a separate express server that will act as a wrapper around media soup and handle all the state management and client connections and sessions , this way we can keep the core api server stateless and also we can easily scale the express server if needed without affecting the core api server , I also made this wrapper open source since I thought it might be useful for other people who want to use mediaSoup
and expirement with it , it is still not a production ready yet! since there are some few stuff missing example of that can be implementing horizonatl scaling (which is not as scary as it sounds when hearing statefull server since mediaSoup already has a mechanism to deal with this in place), all contributions are welcome MediaSoup wrapper
The web proof of concept
it took just few hours to have the media soup up and running , so now I need it to test how everything works together , so I created simple web application that just connects to the api and starts a screenshare session while other clients , watch that screenshare , I tweaked the api a bit , modified ips stuff , but eventually it worked like a charm , better thatn I even Imagined , the delay was very low and the quality was great , I was very happy with the results and I thought that we are good to go and start working on the flutter library and integration with the core api server .
The Flutter library modification journey
Well ..., after the proof of concept was done , I went looking for flutter libraries that can work with media soup , and guess what ? the only libraray I found had the last commits 2 years ago at that time , but I still was optimistic since it is mostly the same protocol and the same architecure , how bad can it be right?, so with my previous experience in flutter I ran the app locally added the libraray , integrated with our server and started testing , and ironicaly media soup V3 decided to change the way that it handles dtls handshakes and session management , you can imagine how that went , but still I said how bad can it be (spoilers alert it was very bad), I spent a week trying to tweak the library, going deeper and deeper into the layer of abstraction and down the rabit hole as well , until I finally gave up and said that this is not the right solution for us , we need to look for other solutions that are more compatible with flutter and have better support for it , so I went back to the drawing board and started looking for other solutions that can fit out solutions .
Also I already wasted 4-5 days trying to do this so for the next parts I had to move quickly
Searching for the correct tool (2 days left)
After spending almost a week trying to make MediaSoup work with Flutter, it became clear that continuing wasn't the right decision.
With only two days left before the demo, I went back to researching alternatives.
After another long night of searching I narrowed the remaining candidates down to three.
Janus
Janus is a very powerfull battle tested sfu that many big companies used including slack when they first launched their calls feature back on 2016 , it has a very good documentation but it also had a lot of features and a lot of integration to setup which made it very hard for use to fully understand the documentation and how to integrate it in only 2 days.
MediaMTX
MediaMtx is a promising famous repo with over 18k stars on github , however internally it is not an sfu per say but a Media server and proxy so it is similar to an MCU rather than an SFU, tho it had a good features I decided to move away from it in the meantime
SRS (Simple Realtime Server)
SRS is a simple and easy to use streaming server that supports variety of protocols which includes (hls, rtmp , webrtc, sfu , mcu ,whip-whep...etc) , And we were already hosting it on our own server for normal hls streaming purposes so we had a lot of experience with it and we knew how to integrate it with our existing infrastructure , it also had a very good documentation and a very active community which made it very easy for us to understand how to use it and how to integrate it with our existing infrastructure .
There was also another solution I never evaluated: LiveKit. Had I looked at it earlier I might have gone with it instead, although given the time constraints I still think familiarity and existing infrastructure would probably have outweighed technical elegance.
What we ended up choosing
At this point you can probably guess what we chose.
We ended up going with SRS for several reasons:
- We were already hosting it.
- We already knew how to operate it.
- Integration with our existing infrastructure was straightforward.
- Documentation was excellent.
- The community was active.
- It fit the timeline better than every other option.
Final Thoughts
If I want the reader to take away just one thing from this story, it's pragmatism.
Most of the time, you won't have the luxury of unlimited time or constraint-free requirements. Engineering isn't always about choosing the technically best solution—it's about choosing the solution that best fits your constraints, your team, and your timeline.
That's exactly what happened here. SRS wasn't necessarily the "best" solution on paper, but it was the one that fit our constraints, integrated well with our existing infrastructure, and allowed us to deliver the feature on time. In the end, that's what mattered.
Top comments (0)