Friday, March 15, 2013

mp3server

A few weeks ago I started writing a server to re-stream an mp3-stream. It all started on IRC (like everything on the internet does):

* klaxa is listening to millie - Dreaming Forest
<Sean_McG> klaxa: you play good music -- do you stream it at all?
<klaxa> no, should i?
* Sean_McG would listen to it
<klaxa> it wouldn't be up all the time, but i guess i can set something up

So I thought of ways how to do it:

<klaxa> i have mpd running, i'd try to add some output that i grab locally via http, have something connect to my server which then encodes to mulitple formats and streams
<klaxa> the hardest part i see right now is the streaming part
<klaxa> the rest shouldn't be too hard, i could even do it with netcat
<klaxa> if i knew how robust mp3 is against starting to stream from just anywhere in the bytestream i could write my own server

Well, turns out mp3 isn't robust  against starting "just anywhere in the bytestream" AT ALL. Therefore I read the mp3 header specification and started implementing a simple program to split an mp3 stream into frames. This alone took some hours of coding, but it was totally worth it. After about a week in total I had written some naive code that could re-stream an mp3-stream to some clients. However, because of the design at the time, all the sockets were blocking and not multiplexed. One night I went to the library (I love working late in the evening) and started to rewrite my code to use select() to multiplex through the clients. Shit worked and we did some first tests which were kinda passed.
Later on Sean_McG started to contribute code, at first it was only to make the server IPv6 compatible, but later he added autotools support and provided many fixes for my poorly written code.

What motivated me to do this was the fact that most streaming software I used until now (shoutcast and mpd) are resource clogging monsters. What I wanted was an extremely lightweight application, which did nothing but data redirection at its core, and this is what evolved from it.

Today and yesterday I did some benchmarks with people from IRC (Thanks Kabaka, LordV, Phase4, Hans_Henrik and Reiuji) to see how well it scales.
Yesterday's test results were basically:
<klaxa> Sean_McG: i got some people benchmark the stream with me and it's at least able to saturate 100 mbit/s
Today I decided to test my server on a different box with more bandwidth. The test results can be boiled down to:
<klaxa> okay so what broke it was probably the filedescriptor limit of 1024
The bandwidth usage of todays test maxed out at about 40 MB/s so circa 320 Mbit/s, serving almost 1000 clients.

A bit insight on the technical details:
The server opens a port (currently hardcoded port 8080) and waits for a client to connect. This client is taken as the source for the mp3-stream (There is no authentication method or anything similar as of now). Each client that connects now will be treated as a "real" client, which wants to listen to our stream.
Now this is where it gets interesting: Whereas mpd uses a buffer for every client and uses some multi-threaded model to push out the stream over HTTP, I implemented a ringbuffer for the stream which every client can access. "Client" in this case is a struct storing the client's filedescriptor, a counter of how many bytes have already been written other data for various things and pointers to the previous and next client struct. In the main-loop select() checks whether or not a new frame from the source can be read, new clients want to connect, or existing clients can be written. In the first case we just remove the oldest frame from the ringbuffer and replace it with the new one. In the second case a new client struct is created and added at the end of the doubly linked client list. In the third case we traverse the client list and write as many frames as possible, in most cases this should be one frame. It is possible that a client lags behind and new frames got added to the ringbuffer without new frames being written to this particular client. In that case upon the next iteration in the main-loop the server will try to write all frames to the client that are needed to be up-to-date with the stream. If clients lag behind one round in the ringbuffer they will experience skips in the stream, if they lag behind two rounds in the ringbuffer they are declared dead and dropped.

My future plans entail:
  • Implement icy-metadata
  • Fork project and rewrite it to work with the most popular audio codecs.
  • Become a real low-resource alternative to icecast.

No comments:

Post a Comment