Creating Online Streaming Radio With Rails and Icecast

Ilya Bodrov

Hello and welcome to this article! Today I would like to talk about creating an online streaming radio with the Ruby on Rails framework. This task is not that simple but it appears that by selecting the proper tools it can be solved without any big difficulties. By applying the concepts described in this tutorial you will be able to create your own radio station, share it with the world and stream any music you like.

Our main technologies for today are going to be Rails (what a surprise!), Icecast streaming server, Sidekiq (to run background jobs), ruby-shout (to manage Icecast) and Shrine (to perform file uploading). In this article we will discuss the following topics:

  • Creating a simple admin page to manage songs that will be streamed on the radio
  • Adding file uploading functionality with the help of Shrine
  • Storing the uploaded files on Amazon S3
  • Installing and configuring Icecast
  • Creating a background job powered by Sidekiq
  • Using ruby-shout gem to perform the actual streaming and managing the Icecast server
  • Displaying the HTML5 player and connecting to the stream
  • Displaying additional information about the currently played track with the help of XSL template and JSONP callback
  • Fetching and displaying even more meta information about the song (like its bitrate)

Sounds promising, eh? If you would like to see how the result may look like, visit Kalinka.fm — an online radio created by me that streams Russian national music. Of course, our demo application won't be that stylish, but the underlying concepts will be absolutely the same. So, let's get started, shall we?

Laying Foundations

We, as total dev geeks, are listening to a geek music, right (whatever it means)? So, the radio that we are going to create will, of course, be for the geeks only. Therefore create a new Rails application:

rails new GeekRadio -T

I am going to use Rails 5.1 for this demo. The described concepts can be applied to earlier versions as well, but some commands may differ (for example, you should write rake generate, not rails generate in Rails 4).

First of all, we will require a model called Song. The corresponding songs table is going to store all the musical tracks played on our radio. The table will have the following fields:

  • title (string)
  • singer (string)
  • track_data (text) — will be utilized by the Shrine gem which is going to take care of the file uploading functionality. Note that this field must have a _data postfix as instructed by the docs.

Run the following commands to create and apply a migration as well as the corresponding model:

rails g model Song title:string singer:string track_data:text
rails db:migrate

Creating Controller

Now let's create a controller to manage our songs:

# app/controllers/songs_controller.rb

class SongsController < ApplicationController
  layout 'admin'

  def index
    @songs = Song.all
  end

  def new
    @song = Song.new
  end

  def create
    @song = Song.new song_params
    if @song.save
      redirect_to songs_path
    else
      render :new
    end
  end

  private

  def song_params
    params.require(:song).permit(:title, :singer, :track)
  end
end

This is a very trivial controller, but there are two things to note here:

  • We are using a separate admin layout. This is because the main page of the website (with the actual radio player) will need to contain a different set of scripts and styles.
  • Even though our column is named track_data, we are permitting the :track attribute inside the song_params private method. This is perfectly okay with Shrine — track_data will only be used internally by the gem.

    Adding a Separate Layout

    Now let's create an admin layout which is going to include a separate admin.js script and have no styling (though, of course, you are free to style it anything you like):

<!-- app/views/layouts/admin.html.erb -->

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>GeekRadio Admin</title>
  <%= csrf_meta_tags %>

  <%= javascript_include_tag 'admin', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<%= yield %>
</body>
</html>

The app/assets/javascripts/admin.js file is going to have the following contents:

// app/assets/javascripts/admin.js
//= require rails-ujs
//= require turbolinks

So, we are adding the built-in Unobtrusive JavaScript adapter for Rails and Turbolinks to make our pages load much faster.

Also note that the admin.js should be manually added to the precompile array. Otherwise, when you deploy to production this file and all the required libraries won't be properly prepared and you will end up with an error:

# config/initializers/assets.rb
# ...
Rails.application.config.assets.precompile += %w( admin.js )

Creating Views and Adding Routes

Now it is time to add some views and partials. Start with the index.html.erb:

<!-- app/views/songs/index.html.erb -->
<h1>Songs</h1>

<p><%= link_to 'Add song', new_song_path %></p>

<%= render @songs %>

In order for the render @songs construct to work properly we need to create a _song.html.erb partial that will be used to display each song from the collection:

<!-- app/views/songs/_song.html.erb -->
<div>
  <p><strong><%= song.title %></strong> by <%= song.singer %></p>
</div>
<hr>

Now the new view:

<!-- app/views/songs/new.html.erb -->
<h1>Add song</h1>

<%= render 'form', song: @song %>

And the _form partial:

<!-- app/views/songs/_form.html.erb -->
<%= form_with model: song do |f| %>
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>

  <div>
    <%= f.label :singer %>
    <%= f.text_field :singer %>
  </div>

  <div>
    <%= f.label :track %>
    <%= f.file_field :track %>
  </div>

  <%= f.submit %>
<% end %>

Note that here I am also using the :track attribute, not :track_data, just like in the SongsController.

Lastly, add the necessary routes:

# config/routes.rb
# ...
resources :songs, only: %i[new create index]

Adding the Upload Functionality

The next step involves integrating the Shrine gem that will allow to easily provide support for file uploads. After all, the site's administrator should have a way to add some songs to be played on the radio. Surely, there are a bunch of other file uploading solutions, but I have chosen Shrine because of the following reasons:

  • It is actively maintained
  • It is really easy to get started
  • I has a very nice plugin system that allows you to cherry-pick only the features that you actually need
  • It is really powerful and allows to easily customize the process of file uploading (for example, you can easily add metadata for each file)

But still, if for some reason you prefer other file uploading solution — no problem, the core functionality of the application will be nearly the same.

So, drop two new gems into the Gemfile:

# Gemfile
# ...
gem 'shrine'
gem "aws-sdk-s3", '~> 1.2'

Note that we are also adding the aws-sdk-s3 gem because I'd like to store our files on Amazon S3 right away. You may ask why haven't I included aws-sdk gem instead? Well, recently this library has undergone some major changes and various modules are now split into different gems. This is actually a good thing because you can choose only the components that will be really used.

Install the gems:

bundle install

Now create an initializer with the following contents:

# config/initializers/shrine.rb

require "shrine"
require "shrine/storage/file_system"
require "shrine/storage/s3"

s3_options = {
    access_key_id:      ENV['S3_KEY'],
    secret_access_key:  ENV['S3_SECRET'],
    region:             ENV['S3_REGION'],
    bucket:             ENV['S3_BUCKET']
}

Shrine.storages = {
    cache: Shrine::Storage::FileSystem.new("tmp/uploads"),
    store: Shrine::Storage::S3.new(upload_options: {acl: "public-read"}, prefix: "store",
                                   **s3_options),
}

Shrine.plugin :activerecord

Here I am using ENV to store my keys and other S3 settings because I don't want them to be publically exposed. I will explain in a moment how to populate ENV with all the necessary values.

Shrine has two storage locations: one for caching (file system, in our case) and one for permanent storage (S3). Apart from keys, region and bucket, we are specifying two more settings for S3 storage:

  • upload_options set ACL (access control list) settings for the uploaded files. In this case we are providing the public-read permission which means that all the files can be read by everyone. After all, that's a public radio, isn't it?
  • prefix means that all the uploaded files will end up in a store folder. So, for example, if your bucket is named scotch, the path to the file will be something like https://scotch.s3.amazonaws.com/store/your_file.mp3

Lastly, Shrine.plugin :activerecord enables support for ActiveRecord. Shrine also has a :sequel plugin by the way.

Now, what about the environment variables that will contain S3 settings? You may load them with the help of dotenv-rails gem really quickly. Add a new gem:

# Gemfile

gem 'dotenv-rails'

Install it:

bundle install

Then create an .env file in the root of your project:

# .env
S3_KEY: YOUR_KEY
S3_SECRET: YOUR_SECRET
S3_BUCKET: YOUR_BUCKET
S3_REGION: YOUR_REGION

This file should be ignored by Git because you do not want to push it accidently to GitHub or Bitbucket. Therefore let's exclude it from version control by adding the following line to .gitignore:

# .gitignore
.env

Okay, we've provided some global configuration for Shrine and now it is time to create a special uploader class:

# app/uploaders/song_uploader.rb

class SongUploader < Shrine
end

Inside the uploader you may require additional plugins and customize everything as needed.

Lastly, include the uploader in the model and also add some basic validations:

# app/models/song.rb

class Song < ApplicationRecord
  include SongUploader[:track]

  validates :title, presence: true
  validates :singer, presence: true
  validates :track, presence: true
end

Note that Shrine has a bunch of special validation helpers that you may utilize as needed. I won't do it here because, after all, this is not an article about Shrine.

Also, for our own convenience, let's tweak the _song.html.erb partial a bit to display a public link to the file:

<!-- app/views/songs/_song.html.erb -->
<div>
  <p><strong><%= song.title %></strong> by <%= song.singer %></p>
  <%= link_to 'Listen', song.track.url(public: true) %> <!-- add this line -->
</div>
<hr>

Our job in this section is done. You may now boot the server by running

rails s

and try uploading some of your favourite tracks.

Icecast

We are now ready to proceed to the next, a bit more complex part of this tutorial, where I'll show you how to install and configure Icecast.

As mentioned above, Icecast is a streaming media server that can work with both audio and video, supporting Ogg, Opus, WebM and MP3 streams. It works on all major operating systems and provides all the necessary tools for us to quite easily enable streaming radio functionality, so we are going to use it in this article.

First navigate to the Downloads section and pick the version that works for you (at the time of writing this article the newest version was 2.4.3). Installation instructions for Linux users can be found on this page, whereas Windows users can simply use the installation wizard. Note, however, that Icecast has a bunch of prerequisites, so make sure you have all the necessary components on your PC.

Before booting the server, however, we need to tweak some configuration options. All Icecast global configuration is provided in the icecast.xml file in the root of the installation directory. There are lots of settings that you can modify but I will list only the ones that we really require:

<!-- installation_path/icecast/icecast.xml -->

<hostname>localhost</hostname>

<authentication>
    <!-- Sources log in with username 'source' -->
    <source-password>PASSWORD</source-password>

    <!-- Admin logs in with the username given below -->
    <admin-user>admin</admin-user>
    <admin-password>PASSWORD3</admin-password>
</authentication>

<shoutcast-mount>/stream</shoutcast-mount>

<listen-socket>
    <port>35689</port>
</listen-socket>

<http-headers>
    <header name="Access-Control-Allow-Origin" value="*" />
</http-headers>

So, we need to specify the following options:

  • Hostname that Icecast will utilize.
  • Two passwords: one for the "source" (that will be used later to manipulate Icecast with some Ruby code) and one for the admin (the guy who can use the web interface to view the status of the server)
  • Mount path to /stream.
  • Port to listen to (35689). It means that in order to access our radio we will need to use the http://localhost:35689/stream URL.
  • Access-Control-Allow-Origin header to easily embed the stream on other websites.

After you done with the settings, Icecast can be started by running the icecast file from the command line interface (for Windows there is an icecast.bat file). You may also visit the http://localhost:35689 to see a pretty minimalistic web interface. Note that in order to visit the Administration section, you will need to provide an admin's password. Great, now our Icecast sever is up and running but we need to manipulate it somehow and perform the actual streaming. Let's proceed to the next section and take care of that!

Sidekiq and Ruby-Shout

Icecast provides bindings for a handful of popular languages, including Python, Java and Ruby. The gem for Ruby is called ruby-shout and we are going to utilize it in this article. Ruby-shout allows us to easily connect to Icecast, change information about the server (like its name or genre of the music) and, of course, perform the actual streaming.

Ruby-shout relies on the libshout base library that can also be downloaded from the official website. The problem is that this library is not really designed to work with Windows (there might be a way to compile it but I have not found it). So, if you are on Windows, you'll need to stick with Cygwin. Install libshout from there and perform all the commands listed below from the Cygwin CLI.

Drop ruby-shout into the Gemfile:

# Gemfile
# ...
gem 'ruby-shout'

We also need decide what tool are we going to use to perform streaming in the background. There are multiple possible ways to solve this task but I propose to stick with a popular gem called Sidekiq that makes working with background jobs a breeze:

# Gemfile
# ...
gem 'sidekiq'

Now install everything:

bundle install

One thing to note is that Sidekiq relies on Redis, so don't forget to install and run it as well.

So, the idea is quite simple:

  • We are going to have a special worker running in the background.
  • This worker will be managed by Sidekiq and started from an initializer file (though in production it is advised to create a separate service).
  • The worker will utilize ruby-shout to connect to the server, stream the uploaded tracks and update information about the currently played song (like its title and performer).
  • The currently played song will have a current attribute set to true.

Before creating our worker, let's generate a new migration to add a current field to the songs table:

rails g migration add_current_to_songs current:boolean:index

Tweak the migration a bit to make the current attribute default to false:

# db/migrate/xyz_add_current_to_songs.rb
# ...
add_column :songs, :current, :boolean, default: false

Apply the migration:

rails db:migrate

Now create a new Sidekiq worker:

# app/workers/radio_worker.rb

require 'shout'
require 'open-uri'
class RadioWorker
  include Sidekiq::Worker
  def perform(*_args)
  end
end

shout is our ruby-shout gem, whereas open-uri will be used to open the tracks uploaded to Amazon.

Inside the perform action we firstly need to connect to Icecast and provide some settings:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    s = Shout.new
    s.mount = "/stream"
    s.charset = "UTF-8"
    s.port = 35689
    s.host = 'localhost'
    s.user = ENV['ICECAST_USER']
    s.pass = ENV['ICECAST_PASSWORD']
    s.format = Shout::MP3
    s.description = 'Geek Radio'
    s.connect
end

The username and the password are taken from the ENV so add two more line to the .env file:

# .env
# ...
ICECAST_USER: source
ICECAST_PASSWORD: PASSWORD_FOR_SOURCE

The username is always source whereas the password should be the same as the one you specified in the icecast.xml inside the source-password tag.

Now I'd like to keep track of the previously played song and iterate over all the songs in an endless loop:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    # ...
    s.connect
    prev_song = nil

    loop do
        Song.where(current: true).each do |song|
            song.toggle! :current
        end
        Song.order('created_at DESC').each do |song|
            prev_song.toggle!(:current) if prev_song
            song.toggle! :current
        end
    end

    s.disconnect
end

Before iterating over the songs, we are making sure the current attribute is set to false for all the songs (just to be on a safe side, because the worker might crash). Then we take one song after another and mark it as current.

Now let's open the track file, add information about the currently played song and perform the actual streaming:

# app/workers/radio_worker.rb
# ...
def perform(*_args)
    # ...
    loop do
        # ...
        Song.order('created_at DESC').each do |song|
            prev_song.toggle!(:current) if prev_song
            song.toggle! :current

            open(song.track.url(public: true)) do |file|
                m = ShoutMetadata.new
                m.add 'filename', song.track.original_filename
                m.add 'title', song.title
                m.add 'artist', song.singer
                s.metadata = m

                while data = file.read(16384)
                    s.send data
                    s.sync
                end
            end
            prev_song = song
        end
    end
end

Here we are opening the track file using the open-uri library and set some metadata about the currently played song. original_filename is the method provided by Shrine (it stored the original filename internally), whereas title and singer are just the model's attributes. Then we are reading the file (16384 is the block size) and send portions of it to Icecast.

Here is the final version of the worker:

# app/workers/radio_worker.rb

require 'shout'
require 'open-uri'
class RadioWorker
  include Sidekiq::Worker
  def perform(*_args)
    prev_song = nil
    s = Shout.new # ruby-shout instance
    s.mount = "/stream" # our mountpoint
    s.charset = "UTF-8"
    s.port = 35689 # the port we've specified earlier
    s.host = 'localhost' # hostname
    s.user = ENV['ICECAST_USER'] # credentials
    s.pass = ENV['ICECAST_PASSWORD']
    s.format = Shout::MP3 # format is MP3
    s.description = 'Geek Radio' # an arbitrary name
    s.connect 

    loop do # endless loop to peform streaming
      Song.where(current: true).each do |song| # make sure all songs are not `current`
        song.toggle! :current
      end
      Song.order('created_at DESC').each do |song|
        prev_song.toggle!(:current) if prev_song # if there was a previously played song, set `current` to `false`
        song.toggle! :current # a new song is playing so it is `current` now

        open(song.track.url(public: true)) do |file| # open the public URL
          m = ShoutMetadata.new # add metadata
          m.add 'filename', song.track.original_filename
          m.add 'title', song.title
          m.add 'artist', song.singer
          s.metadata = m

          while data = file.read(16384) # read the portions of the file
            s.send data # send portion of the file to Icecast
            s.sync
          end
        end
        prev_song = song # the song has finished playing
      end
    end # end of the endless loop

    s.disconnect # disconnect from the server
  end
end

In order to run this worker upon server boot, create a new initializer:

# config/initializers/sidekiq.rb

RadioWorker.perform_async

To see everything it in action, make sure Icecast is started, then boot Sidekiq:

bundle exec sidekiq

and start the server:

rails s

In the Sidekiq's console you should see a similar output:

The line

2017-11-01T17:30:03.727Z 7752 TID-5xikpsg RadioWorker JID-57462d72129bbd0655d2e853 INFO: start

means that our background job is running which means that the radio is now live! Try visiting http://localhost:35689/stream — you should hear your music playing.

Nice, the streaming functionality is done! Our next task is to display the actual player and connect to the stream, so proceed to the next part.

Connecting to the Stream

I would like to display radio player on the root page of our website. Let's create a new controller to manage semi-static website pages:

# app/controllers/pages_controller.rb

class PagesController < ApplicationController
end

Add a new root route:

# config/routes.rb
# ...
root 'pages#index'

Now create a new view with an audio element:

<!-- app/views/pages/index.html.erb -->
<h1>Geek Radio</h1>

<div id="js-player-wrapper">
  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Of course, audio will render a very minimalistic player with a very limited set of customization options. There are a handful of third-party libraries allowing to replace the built-in player with something more customizable, but I think for the purposes of this article the generic solution will work just fine.

Now it is time to write some JavaScript code. I would actually like to stick with the jQuery to simplify manipulating the elements and performing AJAX calls later, but the same task can be solved with vanialla JS. Add a new gem to the Gemfile (newer versions of Rails do no have jquery-rails anymore):

# Gemfile
gem 'jquery-rails'

Install it:

bundle install

Tweak the app/assets/javascripts/application.js file to include jQuery and our custom player.coffee which will be created in a moment:

// app/assets/javascripts/application.js
//= require jquery3
//= require player

Note that we do not need rails-ujs or Turbolinks here because our main section of the site is very simple and consists of only one page.

Now create the app/assets/javascripts/player.coffee file (be very careful about indents as CoffeeScript relies heavily on them):

# app/assets/javascripts/player.coffee
$ ->
  wrapper = $('#js-player-wrapper')
  player = wrapper.find('#js-player')
  player.append '<source src="http://localhost:35689/stream">'
  player.get(0).play()

I've decided to be more dynamic here but you may add the source tag right inside your view. Note that in order to manipulate the player, you need to turn jQuery wrapped set to a JavaScript node by saying player.get(0).

Visit the http://localhost:3000 and make sure the player is there and geek radio is actually live!

Displaying Information About the Song

The radio is now working, but the problem is the users do not see what track is currently playing. Some people may not even care about this, but when I hear some good composition, I really want to know who sings it. So, let's introduce this functionality now.

What's interesting, there is no obvious way to see the name of the currently played song even though this meta information is added by us inside the RadioWorker. It appears that the easiest solution would be to stick with good old XSL templates (well, maybe they are not that good actually). Go to the directory where Icecast is installed, open the web folder and create a new .xsl file there, for example info.xsl. Now it's time for some ugly-looking code:

<!-- installation_path/icecast/web/info.xsl -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:output omit-xml-declaration="yes" method="text" indent="no" media-type="text/javascript" encoding="UTF-8"/>
    <xsl:strip-space elements="*"/>
    <xsl:template match="/icestats">
        parseMusic({
        <xsl:for-each select="source">
            "<xsl:value-of select="@mount"/>":
            {
                "server_name":"<xsl:value-of select="server_name"/>",
                "listeners":"<xsl:value-of select="listeners"/>",
                "description":"<xsl:value-of select="server_description" />",
                "title":"<xsl:if test="artist"><xsl:value-of select="artist" /> - </xsl:if><xsl:value-of select="title" />",
                "genre":"<xsl:value-of select="genre" />"
            }
            <xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
        </xsl:for-each>
        });
    </xsl:template>
</xsl:stylesheet>

You might ask: "What's going on here?". Well, this file generates a JSONP callback parseMusic for us that displays information about all the streams on the Icecast server. Each stream has the following data:

  • server_name
  • listeners — Icecast updates listeners count internally
  • description
  • title — displays both title and the singer's name (if it is available)
  • genre

Now reboot your Icecast and navigate to http://localhost:35689/info.xsl. You should see an output similar to this:

parseMusic({
    "/stream":
    {
        "server_name":"no name",
        "listeners":"0",
        "description":"Geek Radio",
        "title":"some title - some singer",
        "genre":"various"
    }
});

As you see, the mountpoint's URL (/stream in this case) is used as the key. The value is an object with all the necessary info. It means that the JSONP callback is available for us!

Tweak the index view by adding a new section inside the #js-player-wrapper block:

<!-- app/views/pages/index.html.erb -->
<div id="js-player-wrapper">
  <p>
    Now playing: <strong class="now-playing"></strong>
    Listeners: <span class="listeners"></span>
  </p>

  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Next, define two new variables to easily change the contents of the corresponding elements later:

# app/assets/javascripts/player.coffee 
$ ->
  wrapper = $('#js-player-wrapper')
  player = wrapper.find('#js-player')
  now_playing = wrapper.find('.now-playing')
  listeners = wrapper.find('.listeners')
  # ...

Create a function to send an AJAX request to our newly added URL:

# app/assets/javascripts/player.coffee
$ ->
  # ... your variables defined here

  updateMetaData = ->
    url = 'http://localhost:35689/info.xsl'
    mount = '/stream'

    $.ajax
      type: 'GET'
      url: url
      async: true
      jsonpCallback: 'parseMusic'
      contentType: "application/json"
      dataType: 'jsonp'
      success: (data) ->
        mount_stat = data[mount]
        now_playing.text mount_stat.title
        listeners.text mount_stat.listeners
      error: (e) -> console.error(e)

We are setting jsonpCallback to parseMusic — this function should be generated for us by the XSL template. Inside the success callback we are then updating the text for the two blocks as needed.

Of course, this data should be updated often, so let's set an interval:

# app/assets/javascripts/player.coffee
$ ->
    # ... variables and function
    player.get(0).play()
    setInterval updateMetaData, 5000

Now each 5 seconds the script will send an asynchronous GET request in order to update the information. Test it out by reloading the root page of the website. The browser's console should have an output similar to this one: Here we can see that the GET requests with the proper callback are regularly sent to Icecast server and that the response contains all the data. Great!

Displaying Additional Information

"That's nice but I want even more info about the played song!", you might say. Well, let me show you a way to extract additional meta information about the track and display it later to the listener. For example, let's fetch the track's bitrate.

In order to do this we'll need two things:

Firstly, add the gem:

# Gemfile
gem 'streamio-ffmpeg'

Note that this gem requires ffmpeg tool be present on your PC, so firstly download and install it. Next, install the gem itself:

bundle install

Now enable a new Shrine plugin:

# config/initializers/shrine.rb
# ...
Shrine.plugin :add_metadata

Tweak the uploader to fetch the song's bitrate:

# app/uploaders/song_uploader.rb

class SongUploader < Shrine
  add_metadata do |io, context|
    song = FFMPEG::Movie.new(io.path)

    {
        bitrate: song.bitrate ? (song.bitrate / 1000) : 0
    }
  end
end

The hash returned by add_metadata will be properly added to the track_data column. This operation will be performed before the song is actually saved, so you do not need to do anything else.

Next tweak the worker a bit to add provide bitrate information:

# app/workers/song_worker.rb
# ...
def perform(*_args)
    # ...
    open(song.track.url(public: true)) do |file|
        # ...
        m.add 'bitrate', song.track.metadata['bitrate'].to_s
    end
end

Don't forget to modify the XSL template:

<!-- installation_path/icecast/web/info.xsl -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output omit-xml-declaration="yes" method="text" indent="no" media-type="text/javascript" encoding="UTF-8"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/icestats">
parseMusic({
<xsl:for-each select="source">
"<xsl:value-of select="@mount"/>":
{
"server_name":"<xsl:value-of select="server_name"/>",
"listeners":"<xsl:value-of select="listeners"/>",
"description":"<xsl:value-of select="server_description" />",
"title":"<xsl:if test="artist"><xsl:value-of select="artist" /> - </xsl:if><xsl:value-of select="title" />",
"genre":"<xsl:value-of select="genre" />",
"bitrate":"<xsl:value-of select="bitrate" />"
}
<xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
</xsl:for-each>
});
</xsl:template></xsl:stylesheet>

Add a new span tag to the view:

<!-- app/views/pages/index.html.erb -->

<div id="js-player-wrapper">
  <p>
    Now playing: <strong class="now-playing"></strong>
    Listeners: <span class="listeners"></span>
    Bitrate: <span class="bitrate"></span>
  </p>

  <audio id="js-player" controls>
    Your browser does not support the <code>audio</code> element. Seems like you are not a geek :(
  </audio>
</div>

Lastly, define a new variable and update bitrate information inside the success callback:

# app/assets/javascripts/player.coffee
$ ->
    bitrate = wrapper.find('.bitrate')
    .ajax
        success: (data) ->
            mount_stat = data[mount]
            now_playing.text mount_stat.title
            listeners.text mount_stat.listeners
            bitrate.text mount_stat.bitrate

Upload a new track and its bitrate should now be displayed for you. This is it! You may use the described approach to provide any other information you like, for example, the track's duration or the album's name. Don't be afrid to experiment!

Conclusion

In this article we have covered lots of different topics and created our own online streaming radio powered by Rails and Icecast. You have seen how to:

  • Add file uploading feature powered by Shrine
  • Make Shrine work with Amazon S3
  • Install and configure Icecast as well as ruby-shout
  • Create a background job powered by Sidekiq to stream our music
  • Use ruby-shout to manage Icecast and perform streaming
  • Display additional information about the currently played track using XSL and JSONP
  • Fetch more information about the uploaded song and display it to the user

Some of these concepts may seem complex for beginners, so don't be surprised if something does not work for you right away (usually, all the installations cause the biggest pain). Don't hesitate to post your questions if you are stuck — together we will surely find a workaround.

I really hope this article was entertaining and useful for you. If you manage to create a public radio station following this guide, do share it with me. I thank you for reading this tutorial and happy coding!

Ilya Bodrov

3 posts

Ilya Bodrov is personal IT teacher, consulting developer, author and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails), Elixir and JavaScript. He enjoys coding, teaching people and learning new things. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.