Chapter 1: Playlists
Playlists list audio streams of any type. They can be used to provide alternatives in case one of the streams is unavailable, or it can provide alternative formats if the client only supports certain formats. Playlists can also be used to script a number of audio streams together in series. Some playlists allow you to reference other playlists, leading to recursive lists, and some provide metadata that goes along with the stream, such as the title of the stream. The most common radio-related playlists are ASX, PLS, ASF, and M3U.
Generally, you want to play all the entries in a playlist in order. Many stations will have a “pre-roll”, often served as a progressively loaded MP3, either announcing the station or containing an advertisement. The exceptions are ASF playlists, where you play the first available stream, and ASX playlists, which have specific rules as described below. Because playlists can reference other playlists and therefore sometimes contain dozens of entries, it is often a good idea to start playing the first track while you are loading the additional playlists.
Generally when retrieving playlists and streams, you’ll want to pass a user-agent string that is unique to your device or application. However, if you suspect that you are unable to play a stream because of your user-agent, you can try falling back to the WMP user-agent: WMPlayer/10.0.0.364 guid/3300AD50-2C39-46C0-AE0A-AC7B8159E203. Note that if you spoof Internet Explorer’s user-agent (which is not recommended) and try accessing a ShoutCast server, you may be served an administration page instead of the stream.
Below is information about each of the common radio-related playlists, including samples showing the format and some Ruby code to parse them. The code samples are taken from a Ruby program that will parse a wide variety of playlists used by radio streams. It takes into account the necessary exceptions and edge cases. The source-code for this Ruby program is provided in Appendix E for your reference.
ASX
ASX playlists are XML-based and often contain a significant amount of metadata. The playlists have extensions of .asx, .wax, .wvx, or .wmx, depending on the type of media referenced by the playlist. URLs are placed in <entry> or <entryref> tags. The <entry> tag contains <ref> tags that reference streams directly. If there are multiple <ref> tags, then only the first successful stream should be played; often, <ref> tags are used to provide multiple format options for the audio. There is also an <entryref> tag; it links to another ASX playlist, whose <entry> tags are included in the current playlist. ASX playlists can only be nested one level deep.
One issue you sometimes run into with ASX is XML that won’t parse. Technically, ASX isn’t actually XML, just very close. Character encoding differences, unescaped characters, misplaced spaces, or even just mistyped pieces of the playlist can cause an XML parses to die. You can work around this issue by manually extracting the most important pieces, which are the URLs. All entries are listed in the file as either <ref href=’…’ /> or <entryref href=’…’ />, and can be extracted using regular expressions.
Here is an example and some Ruby code to parse it:
<asx version = "3.0">
<Title>Oldies 101.1</Title>
<Author>WABC</Author>
<Copyright>2009 www.wabc.fm</Copyright>
<entryref href="http://wabc.fm/ads.asx" />
<REPEAT>
<entry>
<MoreInfo href = "http://www.wabc.fm/" />
<ref href = "mms://stream.wabc.fm/news"/>
<ref href = "http://stream.wabc.fm/news"/>
<Title>Oldies 101.1 - News</Title>
</entry>
</REPEAT>
</asx>
def parse_asx
@format = :asx
x = @content.gsub(/ ?= ?/, '=').gsub(/entryref/i, 'entryref').gsub(/repeat/i, 'repeat')
x = x.gsub(/href/i, 'href').gsub(/entry/i, 'entry').gsub(/title/i, 'title')
x = x.gsub(/author/i, 'author').gsub(/copyright/i, 'copyright').gsub(/ref/i, 'ref')
xml = REXML::Document.new(x) rescue nil
if xml && xml.root
# extract all entry and entryref elements (including ones embedded in repeat elements)
elements = xml.root.get_elements('*').select { |e| %w{entry entryref repeat}.index(e.name) }
elements = elements.collect { |e|
e.name == 'repeat' ?
e.get_elements('*').select { |e| %w{entry entryref repeat}.index(e.name) } :
e
}.flatten
# extract the actual url entries
index = 0
streams = elements.collect { |e|
if e.name == 'entryref'
url = e.attribute('href').value
url.strip != '' ? { :url => fixurl(url), :playlist => self, :index => index } : nil
index += 1
else # entry (each url has the same index)
title = e.elements['title'] ? e.elements['title'].text : nil
author = e.elements['author'] ? e.elements['author'].text : nil
copyright = e.elements['copyright'] ? e.elements['copyright'].text : nil
urls = e.get_elements('ref').collect { |ee| ee.attribute('href').value }
urls.map! { |url|
{ :url => fixurl(url), :title => title, :copyright => copyright,
:author => author, :playlist => self, :index => index }
}
index += 1
urls
end
}.flatten.compact
else
streams = []
end
# handle malformed xml here
if streams.length == 0 && @content.length > 0
streams = @content.scan(/<(ref|entryref)\s+href\s*?=\s*?"([^"]+)"/i).collect { |m|
m.last
}.collect { |url|
{ :url => fixurl(url), :playlist => self }
}
if streams.length == 0 && @content.index('<').nil? && @content.index('>').nil?
# something's really messed up; assume it's a list of urls (m3u format)
streams = @content.split("\n").select { |l|
l.strip != '' && l.strip !~ /^#/
}.collect { |url|
@format = :m3u
{ :url => fixurl(url), :playlist => self }
}
end
end
streams
end
PLS
PLS playlists use a format similar to Windows .ini files. They are used by many media players but not, interestingly enough, supported by WMP. They include limited metadata about the streams. Here is an example and the Ruby code to parse it:
[playlist]
NumberOfEntries=2
File1=mms://stream.wabc.fm/sports
Title1=Sports
Length1=-1
File2=http://stream.wabc.fm/news
Title2=News
Length2=600
def parse_pls
@format = :pls
key_table = {}
@content.split("\n").select { |l| l.index('=') }.each { |l|
key, val = l.split('=')
key_table[key.downcase] = val.strip unless key.nil? || val.nil?
}
num_entries = (key_table['numberofentries'] || 0).to_i
(1..num_entries).collect { |i|
title = key_table["title#{i}"]
file = key_table["file#{i}"]
file ? { :url => fixurl(file), :title => title, :playlist => self, :index => i-1 } : nil
}.compact
end
M3U
M3U playlists were originally developed for Winamp and are now supported by many players. They are flat files with one URL per line, and optional metadata, including title and track length, in comment lines. The entries are meant to be played sequentially. See the example below and corresponding parsing code:
#EXTM3U
#EXTINF:-1,Oldies 101.1 - Sports - http://www.wabc.fm/
mms://stream.wabc.fm/sports
#EXTINF:600,Oldies 101.1 - News - http://www.wabc.fm/
http://stream.wabc.fm/news
def parse_m3u
@format = :m3u
index = 0
@content.split("\n").select { |l| l.strip != '' && l.strip !~ /^#/ }.collect { |l|
s = { :url => fixurl(l), :playlist => self, :index => index }
index += 1
s
}
end
ASF
The ASF playlist format, as I’ve called it, is a deprecated Windows Media Metafile format, that is, however, still often used in a very specific case. Often a playlist will refer to an RTSP stream with http:// instead of mms://. When the client tries to read the stream via HTTP, it receives one of these ASF playlists. This playlist contains a few different ways (normally two) to connect to the actual stream. The stream URLs also normally include a querystring of “?MSWMExt=.asf”. This tells us that we should be actually connecting to the stream with MMS/RTSP. The actual format of the ASF playlist is similar to that of PLS. See the example below with parsing code:
[Reference]
Ref1=http://111.222.111.222/sports?MSWMExt=.asf
Ref2=http://stream.wabc.fm/sports?MSWMExt=.asf
def parse_asf @format = :asf
@content.split("\n").select { |l| l.index('=') }.collect { |l|
key, url = l.split('=')
{ :url => fixurl(url), :playlist => self, :index => 0 } if key =~ /ref[0-9]+/i
}.compact
end
| ← Introduction | Chapter 2: Protocol → |