Monday, November 23, 2009

Audio Streaming to the iPhone, Take Two

I wasn't completely happy with the results of using M3U files. While it did allow me to specify all of the files, I wasn't able to skip forward and back through the song list.

It turns out that the M3U support is there primarily to support live streams. For example, if you have a live stream from a video source (like a TV tuner card), the server records it, splits it into small chunks and converts it to H.264. The iPhone will then use the M3U file as an index to the individual data files, refreshing the M3U file occasionally. I'll have to set that up next. :)

So I went back to Google to see what I could find. I had come across the <OBJECT> tag in my previous searches, but I had discarded it as too difficult. I didn't want to work in HTML, and object embedding just seemed dirty. However, if I wanted to get access to skip forward/back, it looked like I was going to have to use it. I went through and modified the perl program from yesterday to produce an HTML page for each M3U file. It wasn't until I got to the end that I realized that I had done it wrong.

Lots of things don't work with the OBJECT tag, primarily the GOTO command. It seems that the proper way to embed things into a document is with the EMBED tag. That isn't to say that the EMBED tag is prettier. It isn't, they are both nasty. It is platform and player specific. Luckily, this only has to work on the iPhone/iPod Touch, so I'm lucky that way. I would hate to have to support this for multiple clients.

The first nasty surprise is that while you can have multiple songs in a list, you are limited to 255 of them. Even worse, they aren't done through object references in the rest of the document, the entire list is in the single object tag. That makes it harder to do dynamic, on the fly modification of the list. No small cgi to do shuffles here!

Still, 255 songs is enough to cover pretty much all of my artist directories.

It also seems that Mobile Safari ignores GOTO commands. In regular Safari, you are able to loop back around to the start of the playlist by putting "QTNEXT255=GOTO0" into the embed tag. Looks like Apple doesn't want playing loops on the iPhone.

Next, the iPhone ignores the "autohref", "autoplay" and "autostart" parameters. It always waits for user interaction. This is because the object is not really embedded, it takes full control of the screen. If it did start automatically, it would cause problems on many other sites. It's a small pain, but we'll survive.

I still wish it properly supported M3U files. The seamless transitions are nice. With the QTNEXT, there is a definite pause between tracks.

Here is the updated Perl code:

#!/usr/bin/perl

use File::Find;
use File::Basename;

use vars qw/*name *dir *prune/;

my $URL_BASE= "http://10.10.10.5/";

my @M3Us;

sub createPlaybackHTMLHeader {
    my ($filename, $target, $song) = @_;

    my $title = basename(dirname($filename));

    # open the file, 
    open(HTML, ">$target");

    # Write the HTML/Head elements,

    print HTML <<END;
<html>
  <head>
    <title>$title</title>
    <meta name="viewport" content="width=device-width; initial-scale=1.25"/>
  </head>
  <body>
    <p>Play all music in the "$title" directory, click the button below</p>
    <embed src="$song">
      autoplay="true"
      controller="true"
END
;
    # close the file.
    close(HTML);
}

sub createPlaybackHTMLFooter {
    my ($filename, $target) = @_;

    # open the file, 
    open(HTML, ">>$target");

    # Write the HTML/Head elements,

    print HTML <<END;
      qtnext255="GOTO0"
    </embed>
  </body>
</html>

END
;
    # close the file.
    close(HTML);
}

sub createPlaybackAddSong {
    my ($filename,$target, $song, $count) = @_;

    open(HTML, ">>$target");
    print HTML <<END;
      qtnext$count="<$song> T<myself>"
END
;
    # close the file.
    close(HTML);
}

sub convertM3UToHTML {
    my ($m3ufile, $target) = @_;

    print "Converting $m3ufile to $target...";

    if ( ! -f $m3ufile ) {
 print "No m3ufile, returning\n";
 return;
    }

    # Open that m3u file, and convert it to a playback html page.
    open(M3U, $m3ufile) or die "Can't open m3u file";
    my @songs=<M3U>;
    close(M3U);

    if (@songs > 255) {
 print "Too many songs! Oh well\n";
 @songs = @songs[0..254];
    } elsif (@songs == 0) {
 print "No songs, returning\n";
 return;
    } else {
 print "No problems!\n";
    }

    my $first_song = shift @songs;

    chomp $first_song;

    createPlaybackHTMLHeader($m3ufile, $target, $first_song);
    my $count = 1;

    foreach my $song (@songs) {
 chomp $song;
 createPlaybackAddSong($m3ufile, $target, $song, $count);
 $count += 1;
    }

    createPlaybackHTMLFooter($m3ufile, $target);
}

sub entering {
    print "entering Directory boundary ", $File::Find::name, "\n";

    push @M3Us, $File::Find::dir;

    if ($File::Find::name =~ /.AppleDouble/) {
 return;
    }

    return sort(@_);
}

sub leaving {
    print "leaving Directory boundary ", $File::Find::name, "\n";

    my $directory = pop @M3Us;

    # open the file, 
    my $source = $directory . "/PLAY.m3u";
    my $target = $directory . "/PLAY_ALL.html";

    convertM3UToHTML($source, $target);
}

sub wanted {
   print "Checking ", $File::Find::name,"\n";

   if ( $File::Find::name =~ /\.mp3/ ) {
       print "MP3 Found ", $File::Find::name, "\n";
       my $url = $File::Find::name;
       $url =~ s/ /%20/g;
       $url =~ s/\/export\///g;
       $url = $URL_BASE . $url;
       foreach my $dir (@M3Us) {
    my $m3u_file = $dir . "/PLAY.m3u";
    open (M3U, ">>$m3u_file") or die "Boom!";
    print M3U "$url\n";
    close(M3U);
       }
   }
}

sub cleaner {
    if ($File::Find::name =~ /PLAY.m3u/ ) {
 print "Removing ", $File::Find::name, "\n";
 unlink($File::Find::name);
    }

    if ($File::Find::name =~ /PLAY_ALL.html/ ) {
 print "Removing ", $File::Find::name, "\n";
 unlink($File::Find::name);
    }
}

find ({ wanted => \&cleaner},"/export/mp3");

find ({ wanted => \&wanted , preprocess => \&entering, postprocess => \&leaving},"/export/mp3");

open(M3U, "/export/mp3/PLAY.m3u") or die "Unable to open play";

my @main_m3u = <M3U>;
close(M3U);

my @music = grep(!/AudioBooks/, @main_m3u);
my @random = sort { int(rand(3))-1 } @music;

open(M3U, ">/export/mp3/MUSIC.m3u") or die "Unable to open MUSIC.m3u";

print M3U @music;
close(M3U);

convertM3UToHTML("/export/mp3/MUSIC.m3u", "/export/mp3/MUSIC_PLAY.html");

open(M3U, ">/export/mp3/RANDOM.m3u") or die "Unable to open RANDOM.m3u";
print M3U @random;
close(M3U);

convertM3UToHTML("/export/mp3/RANDOM.m3u", "/export/mp3/RANDOM_PLAY.html");

No comments: