Barnyard BBS

If you're searching for truth,
You must look in the mirror,
And make sense of what you can't see.

Audio Streaming Notes, Example, and Source Code

Overview

I've spent quite some time working with streaming radio lately, and I thought it might be useful to share some of my experiences.

In case you don't know about my background, I am a volunteer with WMPH 91.7 here in Wilmington.  I maintain their website and streaming systems.

WMPH Technology

Our station runs internally on a software package called WaveStation, by BSI.  WaveStation has been "replaced" by a nearly identical product known as Simian.  This software controls all the content of the station.  This includes all the music, public service announcements, and voice talent.  WaveStation produces most of what goes to both our terrestrial transmitter and our internet broadcaster.

We use SimpleCast as our encoder.  This is where we convert the audio signal produced at our studio into an MP3 stream that is destined for our internet broadcast.  Currently, we encode three versions simultaneously (three levels of quality)

We use Icecast as our internet broadcasting software.  This runs on our webserver, and does the "heavy lifting" of the internet transmission.

Also, we use some custom ASP.Net code for the On Demand stuff.  More detail on that later.

Technology Rationale

  • Why WaveStation?  It's what we have.  It works well, and it would be expensive to replace.  Not to mention, we have a lot of automation based on it.
  • SimpleCast is a great product.  We use it because it makes it very easy to encode several streams simultaneously.  We use it both at the station, and for live remote broadcasts.  Cool little program.
  • We originally used Shoutcast.  There's nothing wrong with Shoutcast.  It's a good product.  We switched to Icecast for a few reasons.  Firstly, I like open source.  Secondly, Shoutcast is dead.  I don't know of any further development on it.  That worries me.  Lastly, Icecast offers some advantages over Shoutcast.
    • Icecast can host multiple "mountpoints" on a single port.  A "mountpoint" is a stream of a particular bitrate and content.  This is very helpful.
    • Icecast can also perform authentication in several ways, such as htpasswd and via scripting.
    • Icecast installs as a service.  This is just plain helpful.  I know you can run Shoutcast with SrvAny.  I did it.  I like this better.
  • I use some ASP.Net code to supplement Icecast's streaming abilities.  Icecast is great; but for a "file-served" operation, like On Demand file streaming, it just wasn't right.  I wrote the code because I wanted more security than just hosting the files in open web space.  Skip to the code

Points of Interest

  • Total Control Radio (WaveStation Database Automation)
    • WaveStation uses standard Access MDB databases for a lot of its work.  This is especially useful because it allows you to modify the WaveStation databases very easily through code.  This is how we make the Total Control Radio program operate.  Total Control Radio is actually powered by two parts:
      • The first part is the website.  The website has its own database and controls all the voting and decision-making.  This is just a simple ASP.Net setup.  The website also provides a XML web-service.  You'll see why that's important in a minute.
      • The second part is a script that runs internally at the station.  Total Control Radio is just a standard BSI log template.  It starts the same way each week.  The script is executed once for each song that is played.  It uses the web-service to determine what should be played next, and makes appropriate changes to the WaveStation program log.
      • Words of caution:  You will likely be tempted to make application (executable) calls from within a program log.  Do not attempt to make changes to the same program log from an application call.  The changes will not be recognized.  Use an external way to run your script, such as Windows Task Scheduler.
      • More words of caution:  Once something loads onto one of the virtual "decks", it will not unload if the log changes.  That means you need to keep your "decks" full.  My log uses lots of REM's for padding.  In addition, the show Total Control Radio works about two songs ahead at all times.
  • Icecast
    • Windows Media Player is just a pain-in-the-ass.  I constantly find myself making accommodations for it.
      • WMP has some odd buffering logic.  This makes it barf whenever it is the first to connect using an Icecast on-demand relay.  I'd love to just not support WMP, but the vast majority of our listeners use it.  For this reason, I cannot use on demand relays in Icecast.  This is not the fault of Icecast in any way, but they are still unusable.
      • WMP will barf on variable bitrate streams.  Don't use them.  If you stream files with my ASP.Net code, make sure you are streaming static bitrate.
      • WMP does not have support for title metadata (unless you are using WMA streams).  If your WMP clients don't see anything besides the station information; nothing is wrong.  This is normal.  Although I have no proof, I surmise that this was just to make mp3 streams "less attractive" compared to WMA streams.
      • If WMP receives a "content-length" HTTP header, if will enable the "Save" option to the users.  If you are writing your own streaming code, remember this.
    • Icecast has really improved in stability since the early versions.
    • If you have interest in writing code to generate Icecast htpasswd files, they just use a standard MD5 hash.

On Demand Streaming Code

I'm frequently asked how to stream things On Demand using Icecast.  This is a common question, so I thought that I'd write up a good answer for everybody.

Icecast is a phenomenal program for broadcasting your content; especially when that content is a continuous stream (like a radio station's broadcast).  However, it's not so good for On Demand type broadcasts (where you want each person to start at "the beginning").  Icecast does have the option to serve files via HTTP, but that's just a download.

I've written two versions of my streaming scripts.  The first (and primary) is written in ASP.Net.  Secondarily, I've got one written in PHP.  PHP isn't my main language, and I built it at the request of a friend.

General note on On Demand streaming:  You're streaming directly from files in these examples.  The way that the files are encoded matters. I've seen cases where a bad encoder didn't include a length header in the file; and things just didn't work right.  When in doubt: try a different file in the streamer.

ASP.Net Streaming Code 

Here some sample ASP.Net code that you can use to stream mp3's for On Demand use. I use something very similar to stream mp3's and shows for WMPH.

I'd like to mention that Kevin Pisarsky has some really helpful code for reading (and writing) ID3 tags in ASP.Net.  I use his code for the On Demand section (singles) of WMPH.  You can use his library to make your scripts more dynamic (where the stream's tags are derived from the files).  I didn't include that in the example, because I wanted to the code to be simple and easy to read.

Here are two working demos of the streaming script in action.  Both streams are mp3, just with different playlist files (as different players like different formats):

The sample track is "Always Believe" by Sapphirecut.  She's a good friend of mine.  You can buy her stuff at CDBaby and iTunes.

Download a working sample here (zip includes both the streamer and a sample mp3).

Sample Code

<%@ WebHandler Language="VB" Class="Streamer" %>

'Streaming MP3 Sample
'Ben Yanis, http://www.barnyardbbs.com
'Creative Commons Attribution 2.5
'http://creativecommons.org/licenses/by/2.5/

Imports System
Imports System.Web

Public Class Streamer : Implements IHttpHandler
   
    Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest

        'You could easily make this dymanic
        'For example, you could pass parameters in the the querystring       
        Dim FileName As String = context.Server.MapPath("StreamSample.mp3")

        'Set the content type, we're gonna send mp3 data
        context.Response.ContentType = "audio/mpeg"
       
        'Name the stream
        context.Response.AppendHeader("icy-name", "Sapphirecut - Always Believe (BarnyardBBS Streaming Demo)")
        'Give your url
        context.Response.AppendHeader("icy-url", "Sapphirecut - Always Believe (BarnyardBBS Streaming Demo)")
        'Note: I often read the ID3's from the file directly.  I use some code written by Kevin Pisarsky (www.pisarsky.com)
        'I left it out of this version for simplicity.
       
       
        'At this point, you might wonder why we don't just use the WriteFile or TransmitFile method...
       
        'These will *work*, however, they also will send the Content-Length HTTP header.  This makes it work
        'more like a download, and less like a stream.
       
        'Although you are using a file as your source, this
        'is a real "stream" of data.  This is important, as Windows Media Player cannot save a stream, but
        'it can easily save a download.  If you want to prevent easy saving and downloading, the streaming
        'method is required.  If you don't care, you probably don't need this script anyway.
       
       
        'Don't buffer the output; send it as it goes
        context.Response.Buffer = False
           
        Const ChunkSize As Integer = 10000
        Dim iStream As System.IO.Stream = Nothing
        Dim Buffer(ChunkSize) As Byte
        Dim CurrentLength As Integer
        Dim DataToRead As Long

        Try

            'Open the file.
            iStream = New System.IO.FileStream(FileName, System.IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read)

            'Total bytes to read
            DataToRead = iStream.Length

            'Read the bytes, send chunks at a time
            While DataToRead > 0

                'Verify that the client is connected.
                If context.Response.IsClientConnected Then

                    'Read the data in Buffer
                    CurrentLength = iStream.Read(Buffer, 0, ChunkSize)

                    'Write the data to the current output stream.
                    context.Response.OutputStream.Write(Buffer, 0, CurrentLength)
                   
                    'We're not buffering output; if we were, we would flush here.

                    ReDim Buffer(ChunkSize) ' Clear the Buffer
                    DataToRead = DataToRead - CurrentLength

                Else

                    'Prevent infinite loop if user disconnects
                    DataToRead = -1

                End If

            End While

        Catch ex As Exception

            'Log your errors, if you're keeping score

        Finally

            If IsNothing(iStream) = False Then
                'Close the file stream
                iStream.Close()
            End If

        End Try
       
    End Sub
   
 
    Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property

End Class

 

PHP Streaming Code 

The PHP version is a port of the same concept used in the ASP.Net section.  Remember to adjust the usleep statement based on your bitrate; it can help lessen the load on your server.

<?php

    //Streaming MP3 Sample
    //Ben Yanis, http://www.barnyardbbs.com
    //Creative Commons Attribution 2.5
    //http://creativecommons.org/licenses/by/2.5/

    //You could easily make this dymanic
    //For example, you could pass parameters in the the querystring      
    $name = './StreamSample.mp3';
    $fp = fopen($name, 'rb');
    
    if (!feof($fp)) {
    
        header("Content-Type: audio/mpeg");
        header("Connection: close");
    
        // Only WinAMP and other enlightened players care about these.  Windows Media Player needs to get them from the ASX
        header("icy-name: Streaming Song Title");
        header("icy-url: http://www.yoursite.com");
    
        //The script can be executing for a fairly long time, as it is sending chunks of data (with an artificial delay too).
        //You could always configure this ahead of time, this is just a safety.
        //This is measured in seconds; so in this case it's 5 minutes.
        set_time_limit(300);
    
        while(!feof($fp)) {
           $buf = fread($fp, 8192);   // Load the buffer with 8192 chunks.  Adjust as required
           echo $buf;
           $bytesSent+=strlen($buf);  // Our running count
           
           ob_flush();
           flush();
           
           //Sleep, make it slow for localhost testing
           //This is a simplistic throttling mechanism; adding a delay after every block of bytes
           //In this case, a block of 8192 bytes of released every 200,000 microsecords; or every 1/5th of a second
           //You can adjust this up or down, based on the bitrate of your stuff
           usleep(200000);
        }
    }
    
    exit;
?>