It has now been almost a year since I originally wrote this article, and thus, the codebase for the project I show here has changed considerably, as well as the tools used. For an updated take on the subject of audio streaming with NodeJS, please refer to this post.
If you still wish to continue reading, refer to the branch ‘pre-express’ on GitHub which reflects perfectly to what I explain on this post.
So recently I was invited to compete on NodeKnockout (similar to RailsRumble) by one of Brazil’s best JS developers, the thing was, I had never done NodeJS in my life, and the competition is like in a few weeks time, so for the sake of not slowing the team down I decided to write an app to get familiar with it.
I started out with the NodeJS Peepcode screencast, which lays down a pretty cool app which I immediately hooked to my Rails side project. A week later I started looking in ways to broadcast a stream of music from VirtualDJ, since I’m a kind of an amateur DJ myself and wanted a way to play stuff to my friends through the internet. Through some research I came across Shoutcast, which, as Wikipedia states, is a “cross-platform proprietary software for streaming media over the Internet”. The cool thing is that they have server packages which you can just download and install on your computer, so since I have a Linode VPS box I got it quickly set up on http://stream.pedromtavares.com.
But having a radio stream is not enough, nor does it have anything to do with programming since it’s just server configuration, so where does Node come in? A Shoutcast stream sends buffered audio data, which is perfectly readable by audio players such as iTunes, Windows Media Player and Winamp because they have internal decoders, but something like an HTML5 audio tag can’t read that buffer directly, and that’s where an HTTP decoding proxy comes in. A quick search on StackOverFlow miraculously got me started with very simple solution that worked on Chrome out of the box. From that example I found the creator of the radio-stream package, which was also the owner of icecast-stack (a rewrite of radio-stream). The icecast-stack repo has some examples in it, being the most relevant to our case the simpleProxy, which was the solid foundation for all my work.
Walkthrough the code
Enough talking, let’s go over the things I did on top of that simpleProxy example and how everything is working together. First things first, the code repository for my radio is hosted on GitHub, so make sure you open it to follow along. Just to clear out the architecture, we have a Shoutcast server (which I’ll call radio server) which sends the stream to our Node HTTP server (which I’ll call proxy), we have our income source (VirtualDJ for my case) which feeds data into the radio server, and the browser, which plays data coming from the proxy.
The most important feature is to, obviously, decode and stream audio, and the code for that is all in the decoder.js file. We basically start an instance of Decoder, and with it also start a stream of raw PCM data, which we’ll basically use to write all data that comes from the stream and later decode to mp3/ogg depending on what is asked from the web server. This stream of raw data on STDIN is also useful for a very cool feature called “Burst-On-Connect”, which is basically a buffer of about 2MB of data that will get played immediately when requested, so we don’t have to make the user wait for the radio server to send data to the proxy for the browser to actually play something — we can just send the buffered data immediately.
So we already have data ready to be streamed in our STDIN that’s getting endlessly fed from the radio server, what now? Now we can have our audio tag (or flash player) point to a url such as /stream.mp3, and route that to our mp3 decoder, which will start an mp3 stream that can be read by the audio tag/flash player, and the same goes for /stream.ogg, which is also necessary because some browsers can’t stream mp3 data.
Ok, so moving away from the decoder and into the web server, we have quite a few things to look at. The second most important feature after streaming the audio is showing the current track being played, and that’s dealt with a really cool browser feature called WebSockets, which is basically a thin connection that the server keeps with each of its clients, sending them messages whenever needed. With that in our hands, it becomes really easy to implement real-time track updates, we just need to treat an event (called a ‘metadata’ event) on the proxy that receives information from the radio server that a new track is playing, and spread that out to all clients connected through WebSockets, which is all done through the Faye package.
Another pretty cool thing that was implemented is the stream reconnection feature, where the proxy will go into standby mode when the input source disconnects from the radio server (thus closing the stream), until another source connects and a new stream is stablished. Node makes this extremely easy to handle since everything in it is event-driven.
But users don’t care about all this crap, so we need to deal with the front-end also. Unfortunately HTML5 audio players will only go so far, so I decided to use an awesome jQuery player called jPlayer, which uses the Audio API (instead of relying on native HTML5 audio players) on a very nice looking player. It also has a fallback to a Flash Player for old browsers, and all of this is handled through a common API, so all the JS you write for the player will work no matter what.
You can check out what became of all this at http://mixradio.fm. “BUT DOES IT SCALE?”, you ask. Well, I haven’t managed to get a lot of people connected at it at once since I finished writing it like 2 days ago, but I did manage to get 10~15 people simultaneously and nobody complained about it, so I guess we’re off to a good start.
I plan on adding more features to the radio such as a chat (for song requests) and a track history (using a database), so stay stuned more things to come. And, of course, since the project is open source, feel free to contribute, I know very little about audio encoding and already stated that I am a Node noob, so the code is far from perfect and could use an expert hand. And also, if you happen to be a DJ yourself, I’ll gladly stream your tracks, just hit me at firstname.lastname@example.org.