Ok, I got fed up not having on-screen metadata display for my icecast
streams with freevo, so I threw this together.  It's a script (in
Perl, sorry ;) that does all the hard work of extracting metadata from
an Icecast stream and then feeding it into mplayer, all while
preserving the slave mode input/output pathways on stdin, stdout, and
stderr.  And yes, there is only one connection to the streaming server
;) 

To use the metadata, which the perl script spits to stderr, I had to
patch the mplayer plugin (to look for the new output).  I also threw a
new option in to wrap all audio calls to mplayer with the script
(which has an argument passing scheme like sudo and nice).  If the
script doesn't see an HTTP URL, it just exec()s the (mplayer)
sub-command and no-one's the wiser.  If there's no Icy-Metadata, it just
feeds the stream unaltered.  Read the comments for more details.

Patch and script attached.  Enjoy.

-- 
----------------------------------------------------------------
Brad Spencer - [EMAIL PROTECTED] - "It's quite nice..."
"S.M.A.K.I.B.B.F.B." - A.J. Rimmer | http://jacknife.org/
Index: src/audio/plugins/mplayer.py
===================================================================
RCS file: /cvsroot/freevo/freevo/src/audio/plugins/mplayer.py,v
retrieving revision 1.38
diff -u -p -u -r1.38 mplayer.py
--- src/audio/plugins/mplayer.py        21 Jul 2004 11:18:26 -0000      1.38
+++ src/audio/plugins/mplayer.py        1 Aug 2004 15:39:45 -0000
@@ -117,9 +117,15 @@ class MPlayer:
             filename = item.url
             
         # Build the MPlayer command
-        mpl = '--prio=%s %s -slave %s' % (config.MPLAYER_NICE,
-                                          config.MPLAYER_CMD,
-                                          config.MPLAYER_ARGS_DEF)
+        if config.MPLAYER_AUDIO_WRAPPER == '':
+            mpl = '--prio=%s %s -slave %s' % (config.MPLAYER_NICE,
+                                              config.MPLAYER_CMD,
+                                              config.MPLAYER_ARGS_DEF)
+        else:                                             
+            mpl = '--prio=%s %s %s -slave %s' % (config.MPLAYER_NICE,
+                                                 config.MPLAYER_AUDIO_WRAPPER,
+                                                 config.MPLAYER_CMD,
+                                                 config.MPLAYER_ARGS_DEF)
 
         if not item.network_play:
             demux = ' %s ' % self.get_demuxer(filename)
@@ -298,5 +304,19 @@ class MPlayerApp(childapp.ChildApp2):
     def stderr_cb(self, line):
         if line.startswith('Failed to open'):
             self.stop_reason = 1
+
+        # Handle the icy_client.pl wrapper's metadata extraction
+        if line.startswith('Metadata-Title: '):
+            title = line[16:]
+            if self.item.info['title'] != title:
+                self.item.info['title'] = title
+                self.player.refresh()
+                
+        if line.startswith('Metadata-Artist: '):
+            artist = line[17:]
+            if self.item.info['artist'] != artist:
+                self.item.info['artist'] = artist
+                self.player.refresh()
+        
         for p in self.stdout_plugins:
             p.stdout(line)
#!/usr/local/bin/perl -w
#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#
#
# icy_client.pl [options] [sub-command [args]] URL
#
# This connects to the URL as an Icecast client asking for metadata
# information.  The metadata is written to stderr as it arrives in a form
# as shown below.
#
# Metadata-Artist: Someone
# Metadata-Title: Somesong
#
# If no sub-command is specified, then the pure audio stream
# (minus the metadata) is written to stdout.
#
# If a sub-command is specified, then this script starts an HTTP Icecast
# server on an emepheral port on localhost, and spawns the sub-command with
# all provided arguments, plus an additional URL argument that the sub-command
# can use to connect to the ephemeral server and receive the pure stream
# without metadata.  This lets clients like mplayer play the stream while
# still giving you the ability to display metadata.  The sub-command will
# receive stdin, and its stdout and stderr will be available.
#
# Non-HTTP URLs (or filenames) are passed to the subcommand unchanged.
#
# Brad Spencer <[EMAIL PROTECTED]>
#
#  -s <str>  Use as the title/artist separator string to split the info
#              received from the server.  Default: "$opt{s}"
#  -n        Don't actually emit the audio stream to stdout
#              Only has an effect when there is no subcommand.
#  -v        Dump verbose HTTP/ICY headers
#  -h        Print this message
#
# For example this:
#
#  icy_client.pl mplayer -slave -quiet http://64.236.34.67:80/stream/1024 
#
# Yum.  Icecast metadata from mplayer with slave mode support.
#
###############################################################################
use 5.6.0;
use Getopt::Std;
use strict;
use Socket;
use IO::Handle;

###############################################################################
# Command-line help.  Simple but to the point

my %opt;
sub printHelp
{
  local $_;
  
  open(HELP, "<$0") || die "Can't print help: $!\n";
  while(defined ($_ = <HELP>)) {
    next if /^\#\!/;
    # Substitute in actual values for defaults
    s/\$opt\{([^\}]+)\}/\"$opt{$1}\"/g;
    print STDERR $_;
    last if /^\#\#/;
  }
  exit 1;
}

###############################################################################
# Do it

$opt{s} = ' - ';
getopts('hnv', \%opt);

# If they asked for help or didn't supply enough arguments...
if(exists $opt{h} or $#ARGV < 0) {
  &printHelp();
}

# What separator?
my $separator = $opt{s};

# Pop the URL from the end of the argument list
my $url = pop @ARGV;

# Is there a sub-command to spawn
my $isSubcommand = ($#ARGV >= 0);

if(not $url =~ m!^http://([^:/]+)(?::(\d+))?(/.*)!) {
  if($isSubcommand) {
    # Just spawn the subcommand without any change (push the $url back on)
    exec @ARGV, $url;
    die "Can't exec: $!\n";
  } else {
    &printHelp();
  }
}
my $host = $1;
my $port = (defined $2) ? $2 : 80;
my $uri = $3;

# Are we emitting the audio to stdout or not?
my $emitAudio = ($isSubcommand == 0 and exists $opt{n}) ? 0 : 1;

# Is audio going to stdout or stderr?
my $out;
my $err = *STDERR;
my $internalUrl;
if($isSubcommand) {
  # Start a simple HTTP server
  my $proto = getprotobyname('tcp');
  socket(Server, PF_INET, SOCK_STREAM, $proto)
      or die "socket: $!\n";
  setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1))
      or die "setsockopt: $!\n";
  bind(Server, sockaddr_in(0, inet_aton("127.0.0.1")))
      or die "bind: $!\n";
  listen(Server, SOMAXCONN)
      or die "listen: $!\n";

  # What port did we get?
  my ($serverPort) = sockaddr_in(getsockname(Server));
  $internalUrl = "http://127.0.0.1:$serverPort/";;
} else {
  $out = *STDOUT;
  $err = *STDERR;
}

# Diagnostics
print $err "Connecting to $host:$port\n" if exists $opt{v};

# Connect to the real server
my $proto = getprotobyname('tcp');
socket(Client, PF_INET, SOCK_STREAM, $proto)
    or die "socket: $!\n";
connect(Client, sockaddr_in($port, inet_aton($host)))
    or die "connect: $!\n";
print $err "Connected to $url\n" if exists $opt{v};
autoflush Client 1;
autoflush $err 1;

# Send the request
my $req = "GET $uri HTTP/1.0\r\n"
    . "Host: $host:$port\r\n"
    . "User-Agent: icy_client/0.0.1\r\n"
    . "Icy-MetaData: 1\r\n"
    . "Connection: close\r\n"
    . "\r\n";
print $err $req if exists $opt{v};
print Client ($req) or die "Can't make request!\n";

$_ = <Client>;
die "Server disconnected before responding\n" if not defined $_;
s/\r\n$//;
# Debug
print $err $_, "\n" if exists $opt{v};
# Is it ok?
if(not m!^(?:ICY|HTTP/1.\d+) 200 !) {
  die "Got bad response: \"$_\"\n";
}

# Read the headers
my $interval = 0;
while(<Client>) {
  # End of headers?
  if(/^[\r\n]$/) {
    last;
  }
  s/\r\n$//;
  if(/^Icy-MetaInt: (\d+)/i) {
    $interval = $1;
  }

  # Debug
  print $err $_, "\n" if exists $opt{v};
}

# Debug
my $readChunk;
if($interval > 0) {
  print $err "Metadata will be sent every $interval bytes\n"
      if exists $opt{v};
  $readChunk = $interval;
} else {
  print $err "No metadata available\n" if exists $opt{v};
  $readChunk = 8192;
}

# Helper to read data with some error handling
sub Read($$$)
{
  my ($fh, $data, $bytes) = @_;

  $$data = '';
  my $n = read($fh, $$data, $bytes);
  die "Shouldn't be a short read on blocking socket!\r\n" if $n != $bytes;
  die "End of stream.\r\n" if $n == 0;
  die "Read error: $!\r\n" if not defined $n;
  return undef;
}

# Fork if necessary
if($isSubcommand) {
  my $pid = fork;
  die "Can't fork: $!\n" if(not defined $pid);
  if($pid == 0) {
    # Run the child with the extra URL argument that points to us
    exec @ARGV, $internalUrl;
    die "Couldn't exec: $!\n";
  }

  # Simple server
  my $paddr = accept(Child, Server);
  die "Accept failed: $!\n" if not $paddr;

  # Read the request and ignore its content for now
  # TODO: make sure they ask for '/'
  while(<Child>) {
    # End of headers?
    if(/^[\r\n]$/) {
      last;
    }
    s/\r\n$//;
    
    # Debug
    print $err "Child: ", $_, "\n" if exists $opt{v};
  }

  # Set the output fd to be the Child
  $out = *Child;

  # Here it comes.  Just say ok and be done with it
  print $out "ICY 200 OK\r\n\r\n";
}

# Now we just read the interval and then the metadata
autoflush $out 1;
my $data;
while(1) {
  # Read a full interval
  &Read(*Client, \$data, $readChunk);

  # Write it out
  print $out $data if $emitAudio;

  # Is there meta-data in the stream?
  if($interval > 0) {
    # Read the meta-data length
    &Read(*Client, \$data, 1);
    my $metaLength = unpack("C", $data);
    
    # Read the meta-data itself
    if($metaLength > 0) {
      # The length byte needs to be multiplied by 16 to get the real length
      &Read(*Client, \$data, $metaLength * 16);
      
      # Find a stream title
      if($data =~ m!StreamTitle\s*=\s*\'(.*?)\';!) {
        # Split it
        my $title = $1;
        my $i = index($title, $separator);
        if($i == -1) {
          # No separator
          print $err "Metadata-Title: $1\n";
        } else {
          print $err ("Metadata-Artist: ", substr($title, 0, $i), "\n",
                      "Metadata-Title: ", substr($title,
                                                 $i + length($separator)),
                      "\n");
        }
      }
    }
  }
}

#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#*#

Attachment: signature.asc
Description: Digital signature

Reply via email to