UPDATE 11/5/07 After inserting that first line of code
set_time_limit(10 * 60);
I seem to have less choppy music. It turns out my server only allowed any PHP script to run for only 60 seconds. This disconnected Safari, and initiated another request from it…causing the overlapping music problem I explained below. I haven’t had time to fully test it, but in theory setting a longer time limit would tone this problem down quite a bit. Ideally, you’d give Safari every byte it asked for, and not bail out because the script is taking too long.
The first post in a brand new blog!
I frequently listen to a certain online radio station, Cerritos All Stars. Since I’m always around the house listening to music on my iPhone, I wanted a way to listen to the station instead of just my music library. Of course I started Google searching ASAP. I found several things:
- There is no Shoutcast client for the iPhone yet
- iPhone’s Safari can play mp3 files which are located online, but not Shoutcast streams
On a Macrumors forum, some people were talking about tricking Safari into thinking that a Shoutcast stream is just a plain mp3 file on a web server. The method they tried was using PHP to fake some HTTP headers: Content-type, Content-Length. After that, just read bytes from the Shoutcast server and pass them on to Safari. This almost works. Safari brings starts up the built-in Quicktime player and seems ready to play, but instead the “play” symbol with a strikeout line through it shows up.
Further on in the forum thread, someone mentioned that Safari downloads the mp3 in chunks. This is to enable simultaneous playing and buffering. This is why the aforementioned method did not work.
- Safari first requests the script
- The script sends mp3 type headers, Safari likes them and starts up its Quicktime player
- Safari asks for bytes 0 and 1 of the mp3, to test for file-resuming capabilities
- The script sends out the same headers
- Safari thinks, “impostor!”
So after doing a little reading about HTTP responses and all that good stuff, I realized that the PHP script needed to be a little more flexible. It needed to pretend to be resuming a file transfer when Safari asked it to. So a little modifying, and bingo.
- Safari first requests the script
- The script sends mp3 type headers, Safari likes them and starts up its Quicktime player
- Safari asks for bytes 0 and 1 of the mp3, to test for file-resuming capabilities
- The script sends HTTP 206 and related headers
- The script sends two bytes of the Shoutcast stream
- Safari thinks, “sweet, gimme more!”
- Safari asks for bytes 0 - x
- The script sends more bytes of the stream
- Quicktime plays bytes of the stream
The quick and dirty code:
set_time_limit(10 * 60); //this could take a while, allowing 10 minutes. just added recently see UPDATE at the top of post
$bytes_to_send = 480000 * 130; //stream about about 2 hours of music
$headers = http_get_request_headers(); //get the HTTP request headers safari has sent
//if safari is only asking for a portion of the "mp3"
if (isset($headers['Range'])) {
$exploded_range = explode('=', $headers['Range']);
$limits = explode('-', $exploded_range[1]);
$length = ($limits[1] - $limits[0]) + 1; //the content length
$content_range = 'bytes ' . $limits[0] . '-' . $limits[1]; //the content range
//send fake HTTP headers to safari, telling it that we're sending only the portion of the "mp3" it asked for
header('HTTP/1.1 206 Partial Content');
header('Accept-Ranges: bytes');
header('Content-Length: ' . $length);
header('Content-Range: ' . $content_range . '/' . $bytes_to_send);
header('Content-type: audio/mpeg');
//open the stream to the shoutcast server, set as resource $fp
$fp = fsockopen("stream.cerritosallstars.com", "80", $errno, $errstr, 30) or die("Unable to connect to server!");
//HTTP commands that will initiate the shoutcast server sending stream data
$buf = "GET / HTTP/1.0\r\nIcy-MetaData:0\r\n\r\n";
//send HTTP commands in string $buf to stream $fp
fwrite($fp, $buf);
//get next line from stream
$buf = fgets($fp, 1024);
//get next few lines and discard them, this is only
//shoutcast data that would sound like noise if iphone played them
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
$buf = fgets($fp, 1024);
//break if EOF
if ($buf == "\r\n") {
break;
}
$bytes_sent = 0;
//while pointer is not at EOF, and not too many bytes are sent...
while (!feof($fp) AND ($bytes_sent < $length)) {
//read 1 byte of stream
$buf = fread($fp, 1);
//output byte to iphone;
echo $buf;
$bytes_sent++;
}
fclose($fp);
exit();
}
//else, it is the initial request. safari is asking for the whole "mp3", and seeing how big it is ($bytes_to_send)
else {
header('Accept-Ranges: bytes');
header('Content-Length: ' . $bytes_to_send);
header('Content-type: audio/mpeg');
echo 'blah';
exit();
}
exit();
So in the end, music played. However, this is a pretty sketchy way of doing things. Because of the way Quicktime buffers, seconds of music are not heard, and seconds of music are repeated. Know what I mean? Every request that happens at the same time will be filled with the same byte. The script can definitely get bytes from the present fine but when Safari requests bytes from the future, it still gets bytes from the present. Here is another way to view the issue.
- Safari asks for bytes 1 - 3
- Script serves bytes 1, 2, 3 and Safari plays them
- While playing byte 2, Safari asks for bytes 4 - 6
- CRAP, byte 3 will be the same as byte 4
A few seconds of repeated music are heard. I can live with this, but theres no way any final solution could have this problem.
I would also like to point out that PHP and Apache have been ported to the iPhone. There might be a way to run the script locally on the iPhone, eliminating the need for another server and extra bandwidth. And another reminder, this would only be ideal on WiFi…iPhone can’t make or receive calls while using EDGE.
If anyone has an elegant way of listening to Shoutcast on iPhone, please leave a comment!