package Audio::Play::MPlayer;
use strict;
use warnings;
use base qw(Class::Accessor::Fast);
use IPC::Open3 qw(open3);
use IO::Handle;
our $VERSION = '0.07';
# FIXME, missing
# url type layer mode mode_extension copyrighted error_protected
# emphasis extension
__PACKAGE__->mk_ro_accessors( qw(frame title artist album year comment
genre samplerate channels bitrate
extension) );
sub new {
my( $class, %args ) = @_;
my $self = bless \%args, $class;
$self->start_mplayer( $args{mplayerargs} );
$self->{state} = 0;
return $self;
}
sub DESTROY {
my( $self ) = @_;
$self->stop_mplayer;
}
sub start_mplayer {
my( $self, $args ) = @_;
my( $wr, $rd );
my $pid = open3( $wr, $rd, $rd,
'mplayer', '-slave', '-idle', '-v', @{$args || []} );
die "Can't start mplayer" unless $pid;
$self->{pid} = $pid;
$self->{r} = $rd;
$self->{w} = $wr;
$self->{r}->blocking( 0 );
$self->{frame} = [ undef, undef, undef, undef ];
$self->{buffer} = '';
}
sub stop_mplayer {
my( $self ) = @_;
return unless $self->{pid};
$self->command( 'quit' );
my $pid = delete $self->{pid};
close delete $self->{r};
close delete $self->{w};
waitpid $pid, 0;
}
sub line {
my( $self, $wait ) = @_;
for(;;) {
# append to buffer
my $len = sysread $self->{r}, $self->{buffer}, 2048,
length( $self->{buffer} );
return $1 if $self->{buffer} =~ s/^([^\n\r]*)[\r\n]+//;
if( $wait ) {
vec( my $rbits = '', fileno( $self->{r} ), 1 ) = 1;
select $rbits, undef, undef, 60;
} else {
return;
}
}
}
my %info =
( meta_title => [ 'get_meta_title', 'title', ],
meta_artist => [ 'get_meta_artist', 'artist', ],
meta_album => [ 'get_meta_album', 'album', ],
meta_year => [ 'get_meta_year', 'year', ],
meta_comment => [ 'get_meta_comment', 'comment', ],
meta_genre => [ 'get_meta_genre', 'genre', ],
);
# AUDIO: 44100 Hz, 2 ch, s16le, 128.0 kbit/9.07% (ratio: 16000->176400)
# A: 16.5 (16.4) of 252.3 (04:12.3) 3.6%
# ===== PAUSE =====
sub parse {
my( $self, $re, $wait ) = @_;
while( my $line = $self->line( $wait ) ) {
if( $line =~ /^EOF code:/ ) {
$self->{state} = 0;
}
elsif($line =~ /^A:\s+([\d\.]+)\s+\([\d\:\.]+\)\s+of\s+([\d\.]+)/ ) {
$self->{frame}->[2] = $1;
$self->{frame}->[3] = $2 - $1;
# FIXME heuristic
$self->{state} = 0 if $self->{frame}->[3] <= 0;
}
elsif( $line =~ /=====\s+PAUSE\s+=====/ ) {
$self->{state} = 1;
}
elsif( $line =~ /^ANS_(\w+)='([^']+)'$/ ) {
# FIXME quoting
my( $k, $v ) = ( lc( $1 ), $2 );
if( $info{$k} ) {
$self->{$info{$k}->[1]} = $v;
}
}
elsif( $line =~ /^AUDIO:\s+(\d+)/ ) {
$self->{samplerate} = $1;
if( $line =~ /(\d+)\s+ch/i ) {
$self->{channels} = $1;
}
if( $line =~ /(\d+)\.\d+\s+kbit/i ) {
$self->{bitrate} = $1;
}
}
elsif( $line =~ /^Playing\s/ ) {
$self->{$_->[1]} = undef foreach values %info;
$self->command( $_->[0] ) foreach values %info;
}
elsif( $line =~ /^\s+(title|artist|album|year|comment|genre):\s(.*?)\s*$/i ) {
# FIXME heuristic
$self->{lc($1)} = $2;
}
else {
# print STDERR $line, "\n";
}
return $line if $line =~ $re;
}
return;
}
sub poll {
my( $self, $wait ) = @_;
$self->parse( qr/./, $wait ); # wait for anything
$self->parse( qr/^\0/, 0 ); # consume pending output
}
sub command {
my( $self, $command ) = @_;
print { $self->{w} } $command, "\n";
}
sub load {
my( $self, $file ) = @_;
# FIXME quoting
$self->command( qq{loadfile "$file"} );
$self->{state} = 2; # feeling lucky
}
sub state {
my( $self ) = @_;
return $self->{state};
}
# FIXME works more-or-less
sub stop {
my( $self ) = @_;
return if $self->{state} == 0;
$self->pause if $self->{state} == 2;
$self->command( 'stop' );
#$self->command( 'pausing_keep seek 0.0 2' );
$self->poll;
$self->{state} = 0;
}
sub pause {
my( $self ) = @_;
return if $self->{state} == 0;
$self->command( "pause" );
if( $self->{state} == 2 ) {
$self->parse( qr/=====\s+PAUSE\s+=====/, 1 );
# try to parse metatdata command answers
$self->poll;
} elsif( $self->{state} == 1 ) {
$self->{state} = 2;
}
}
sub paused { $_[0]->{state} == 2 ? 0 : 1 }
# FIXME not like Audio::Play::MPG123
sub jump {
my( $self, $seconds ) = @_;
if( $seconds && $seconds =~ /^[+\-]/ ) {
$self->command( "seek $seconds 0" );
} else {
$self->command( "seek $seconds 2" );
}
}
# mock Audio::Play::MPG123
sub tpf { 1 }
1;
__END__
=head1 NAME
Audio::Play::MPlayer - a frontend to play audio files using MPlayer
=head1 SYNOPSIS
use Audio::Play::MPlayer;
# same as Audio::Play::MPG123
$player = Audio::Play::MPlayer->new;
$player->load( "ex-mp30.mp3" );
print $player->title, "\n";
$player->poll( 1 ) until $player->state == 0;
=head1 DESCRIPTION
This module acts as a frontend to an external MPlayer process started
with the C<-slave> command-line option. The idea and interface (and
in part the code) has been taken from L<Audio::Play::MPG123>.
Please see L<Audio::Play::MPG123> for the documentation. Take into account
that the methods:
copyrighted
emphasis
error_protected
extension
layer
mode
mode_extension
stat
statfreq
type
url
IN
have not been implemented, and that:
jump
tpf
work differently: C<jump> takes offsets in seconds, and C<tpf> always
returns C<1> to make it possible to write:
$player->jump( 60 / $player->tpf );
=cut
=head1 AUTHOR
Mattia Barbon <mbarbon@cpan.org> and Luke Poskitt <ltp@cpan.org>,
using ideas from L<Audio::Play::MPG123> by Marc Lehmann <schmorp@schmorp.de>.
=head1 LICENSE
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
=head1 SOURCES
The latest sources can be found on GitHub at
L<http://github.com/mbarbon/audio-play-mplayer/tree>
=head1 THANKS
Marc Lehmann <schmorp@schmorp.de> for L<Audio::Play::MPG123>.
The MP3 file used in tests has been taken from
L<http://www.shellworld.net/~davidsky/exam-mp3.htm> (test ID3 tag not
in original).
=cut