1# Copyright (C) 2005-2015 Quentin Sculo <squentin@free.fr> 2# 3# This file is part of Gmusicbrowser. 4# Gmusicbrowser is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License version 3, as 6# published by the Free Software Foundation 7 8package Play_mpv; 9use strict; 10use warnings; 11use IO::Socket::UNIX; 12use JSON::PP; 13use Time::HiRes 'sleep'; 14 15use POSIX ':sys_wait_h'; #for WNOHANG in waitpid 16 17#$SIG{CHLD} = 'IGNORE'; # to make sure there are no zombies #cause crash after displaying a file dialog and then runnning an external command with mandriva's gtk2 18#$SIG{CHLD} = sub { while (waitpid(-1, WNOHANG)>0) {} }; 19 20my (@cmd_and_args,$ChildPID,$WatchTag,$WatchTag2,@pidToKill,$Kill9); 21my $sockfh; 22my (%supported,$mpv); 23my $preparednext; 24my ($File_is_current,$Called_from_eof,$gmb_file,$mpv_file,$Last_messages); 25my $initseek; 26my $watcher; 27 28my $SOCK = $::HomeDir."gmb_mpv_sock"; 29 30$::PlayPacks{Play_mpv}=1; #register the package 31 32sub init 33{ undef %supported; 34 $mpv= $::Options{mpv_cmd}; 35 if ($mpv && !-x $mpv && !(::first { -x $_ } map $_.::SLASH.$mpv, split /:/, $ENV{PATH})) 36 { $mpv=undef; 37 } 38 $mpv ||= ::first { -x $_ } map $_.::SLASH.'mpv', split /:/, $ENV{PATH}; 39 40 $mpv=undef unless $mpv && check_version(); 41 return unless $mpv; 42 return bless {RG=>1,EQ=>1},__PACKAGE__; 43} 44 45sub check_version 46{ return unless $mpv; 47 my $output= qx/$mpv -V/; 48 my $ok= $output=~m/mpv\s*(\d+)\.(\d+)(\S+)?/i && ($1>0 || $2>=7) ? 1 : 0; #requires version 0.7 or later 49 if ($::debug) 50 { if (defined $1) { warn "mpv: found mpv v$1.$2".($3||"")."\n"; } 51 else {warn "mpv: error looking up mpv version\n"} 52 } 53 if (!$ok) { warn "mpv version earlier than 0.7 are not supported -> mpv backend disabled\n" } 54 return $ok; 55} 56 57sub supported_formats 58{ return () unless $mpv; 59 unless (keys %supported) 60 {for (qx($mpv --ad=help)) 61 { if (m/:mp3\s/) {$supported{mp3}=undef} 62 elsif (m/:vorbis\s/) {$supported{oga}=undef} 63 elsif (m/:mpc\d/) {$supported{mpc}=undef} 64 elsif (m/:flac\s/) {$supported{flac}=undef} 65 elsif (m/:wavpack\s/) {$supported{wv}=undef} 66 elsif (m/:ape\s/) {$supported{ape}=undef} 67 elsif (m/:aac\s/) {$supported{m4a}=undef} 68 } 69 } 70 return keys %supported; 71} 72 73sub send_cmd 74{ return unless $sockfh; 75 my @args=@_; 76 my $cmd = JSON::PP->new->encode({command => \@args}); 77 print $sockfh "$cmd\n"; 78 warn "MPVCMD: $cmd\n" if $::debug; 79} 80 81sub launch_mpv 82{ $preparednext=undef; 83 @cmd_and_args=($mpv, '--input-unix-socket='.$SOCK, qw/--idle --no-video --no-input-terminal --really-quiet --gapless-audio=weak --softvol-max=100 --mute=no --no-sub-auto/); 84 push @cmd_and_args,"--volume=".convertvolume($::Volume); 85 push @cmd_and_args,"--af-add=".get_RG_opts() if $::Options{use_replaygain}; 86 push @cmd_and_args,"--af-add=\@EQ:equalizer=$::Options{equalizer}" if $::Options{use_equalizer}; 87 push @cmd_and_args,split / /,$::Options{mpvoptions} if $::Options{mpvoptions}; 88 warn "@cmd_and_args\n" if $::debug; 89 $ChildPID=fork; 90 if (!defined $ChildPID) { warn "gmusicbrowser_mpv : fork failed : $!\n"; ::ErrorPlay("Fork failed : $!"); return } 91 elsif ($ChildPID==0) #child 92 { exec @cmd_and_args or print STDERR "launch failed (@cmd_and_args) : $!\n"; 93 POSIX::_exit(1); 94 } 95 #wait for mpv to establish socket as server 96 for (0 .. 200) 97 { $sockfh = IO::Socket::UNIX->new(Peer => $SOCK, Type => SOCK_STREAM); 98 last if $sockfh || (waitpid($ChildPID, WNOHANG) != 0); 99 warn "gmusicbrowser_mpv: could not connect to socket; retrying\n" if $::debug; 100 sleep 0.01; 101 } 102 unless ($sockfh) 103 { handle_error("failed to connect to socket (probably failed to launch mpv): $!"); 104 return; 105 } 106 $sockfh->autoflush(1); 107 $sockfh->blocking(0); 108 $WatchTag = Glib::IO->add_watch(fileno($sockfh),'hup',\&_eos_cb); 109 $WatchTag2= Glib::IO->add_watch(fileno($sockfh),'in',\&_remotemsg); 110 $watcher = {}; 111 ::Watch($watcher,'NextSongs', \&append_next); 112 send_cmd('observe_property', 1, 'playback-time'); 113 send_cmd('observe_property', 1, 'path'); 114 send_cmd('request_log_messages', 'error'); 115 return 1; 116} 117 118sub Play 119{ my (undef,$file,$sec)=@_; 120 launch_mpv() unless $ChildPID && $sockfh; 121 return unless $ChildPID; 122 $Last_messages=""; 123 $gmb_file=$file; 124 warn "playing $file (pid=$ChildPID)\n" if $::Verbose; 125 # gapless - check for non-user-initiated EOF 126 if ($Called_from_eof && $preparednext && $preparednext eq $gmb_file && $gmb_file eq $mpv_file) 127 { $File_is_current=1; 128 return; 129 } 130 $File_is_current=-1; 131 $initseek = $sec; 132 send_cmd('loadfile',$file); 133 send_cmd('playlist_clear'); 134} 135 136sub append_next 137{ $preparednext=undef; 138 send_cmd('playlist_clear'); 139 if ($::NextFileToPlay && $::NextFileToPlay ne $gmb_file) 140 { send_cmd('loadfile',$::NextFileToPlay,'append'); 141 $preparednext= $::NextFileToPlay; 142 } 143} 144 145sub _remotemsg 146{ my $eof; 147 while (my $line=<$sockfh>) 148 { my $msg= JSON::PP->new->decode($line); # use JSON::PP->new->decode instead of decode_json (equivalent to JSON::PP->new->utf8->decode) because decode_json converts to utf8, which gives error with invalid utf8 filenames (only happens for mpv >0.9.0) 149 warn "mpv raw-output: $line" if $::debug; 150 if (my $error=$msg->{error}) 151 { warn "mpv error: $error" unless $error eq 'success'; 152 } 153 elsif (my $event=$msg->{event}) 154 { if ($event eq 'property-change' && $msg->{name} eq 'path') { $mpv_file= $msg->{data}||""; } # doesn't happen when previous file is same as new file 155 elsif ($event eq 'start-file') { $File_is_current=0 if $File_is_current<1; } 156 elsif ($event eq 'tracks-changed' && $File_is_current>=0) 157 { $File_is_current= $mpv_file eq $gmb_file; 158 last if $eof; #only do eof now to catch log-message that are only sent after end-file and start-file 159 } 160 elsif ($File_is_current<1) {} #ignore all other events unless file is current 161 elsif ($event eq 'property-change' && $msg->{name} eq 'playback-time' && defined $msg->{data}) 162 { ::UpdateTime($msg->{data}); } 163 elsif ($event eq 'end-file') { $eof=1; } # ignore EOF signal on user-initiated track change as $File_is_current is not true in those cases 164 elsif ($event eq 'file-loaded') { SkipTo(undef,$initseek) if $initseek; $initseek=undef; } 165 elsif ($event eq 'log-message') 166 { my $error= $msg->{text}; 167 chomp $error; 168 my $time= $::PlayTime||0; 169 $Last_messages.= sprintf " %02d:%02d [%s] %s\n", 170 int($time/60), $time%60, $msg->{prefix}, $error; 171 } 172 } 173 } 174 handle_eof() if $eof; 175 return 1; 176} 177 178sub handle_eof 179{ if ($::PlayTime < Songs::Get($::SongID,'length')-5) { handle_error(_"Playback ended unexpectedly.") } 180 else { $Called_from_eof=1; ::end_of_file(); $Called_from_eof=0; } 181} 182 183sub handle_error 184{ my $error=shift; 185 Stop(); 186 my $details=_("File").":\n$gmb_file\n\n"; 187 $details.= _("Last messages:")."\n$Last_messages\n" if $Last_messages; 188 $details.= _("Command used:")."\n@cmd_and_args"; 189 ::ErrorPlay($error,$details); 190} 191 192sub _eos_cb 193{ my $error; 194 if ($ChildPID && $ChildPID==waitpid($ChildPID, WNOHANG)) 195 { $error=_"Check your audio settings" if $?; 196 } 197 while (waitpid(-1, WNOHANG)>0) {} #reap dead children 198 handle_error ($error or "mpv process closed unexpectedly."); 199 return 1; 200} 201 202sub Pause 203{ send_cmd('set', 'pause', 'yes'); 204} 205sub Resume 206{ send_cmd('set', 'pause', 'no'); 207} 208 209sub SkipTo 210{ ::setlocale(::LC_NUMERIC, 'C'); 211 my $sec="$_[1]"; 212 ::setlocale(::LC_NUMERIC, ''); 213 send_cmd('seek', $sec, 'absolute'); 214} 215 216 217sub Stop 218{ if ($WatchTag) 219 { Glib::Source->remove($WatchTag); 220 Glib::Source->remove($WatchTag2); 221 $WatchTag=$WatchTag2=undef; 222 } 223 if ($ChildPID) 224 { send_cmd('quit'); 225 Glib::Timeout->add( 100,\&_Kill_timeout ) unless @pidToKill; 226 $Kill9=0; #_Kill_timeout will first try INT, then KILL 227 push @pidToKill,$ChildPID; 228 undef $ChildPID; 229 } 230 if ($sockfh) 231 { shutdown($sockfh,2); 232 close($sockfh); 233 unlink $SOCK; 234 undef $sockfh; 235 } 236 if ($watcher) 237 { ::UnWatch($watcher,'NextSongs'); 238 undef $watcher; 239 } 240} 241sub _Kill_timeout #make sure old children are dead 242{ while (waitpid(-1, WNOHANG)>0) {} #reap dead children 243 @pidToKill=grep kill(0,$_), @pidToKill; #checks to see which ones are still there 244 if (@pidToKill) 245 { warn "Sending ".($Kill9 ? 'KILL' : 'INT')." signal to @pidToKill\n" if $::debug; 246 if ($Kill9) {kill KILL=>@pidToKill;} 247 else {kill INT=>@pidToKill;} 248 $Kill9=1; #use KILL if they are still there next time 249 } 250 return @pidToKill; #removes the timeout if no more @pidToKill 251} 252 253sub AdvancedOptions 254{ my $vbox=Gtk2::VBox->new(::FALSE, 2); 255 my $sg1=Gtk2::SizeGroup->new('horizontal'); 256 my $opt=::NewPrefEntry('mpvoptions',_"mpv options :", sizeg1=>$sg1); 257 $vbox->pack_start($_,::FALSE,::FALSE,2), for $opt; 258 return $vbox; 259} 260 261# Volume functions 262sub GetVolume {$::Volume} 263sub GetMute {$::Mute} 264sub SetVolume 265{ shift; 266 my $set=shift; 267 if ($set eq 'mute') { $::Mute=$::Volume; $::Volume=0; } 268 elsif ($set eq 'unmute') { $::Volume=$::Mute; $::Mute=0; } 269 elsif ($set=~m/^\+(\d+)$/) { $::Volume+=$1; } 270 elsif ($set=~m/^-(\d+)$/) { $::Volume-=$1; } 271 elsif ($set=~m/(\d+)/) { $::Volume =$1; } 272 $::Volume=0 if $::Volume<0; 273 $::Volume=100 if $::Volume>100; 274 my $vol= convertvolume($::Volume); 275 send_cmd('set', 'volume', $vol); 276 ::HasChanged('Vol'); 277 $::Options{Volume}=$::Volume; 278 $::Options{Volume_mute}=$::Mute; 279} 280 281sub convertvolume 282{ my $vol=$_[0]; 283 #$vol= 100*($vol/100)**3; #convert a linear volume to cubic volume scale #doesn't seem to be needed in mpv 284 # will be sent to mpv as string, make sure it use a dot as decimal separator 285 ::setlocale(::LC_NUMERIC, 'C'); 286 $vol="$vol"; 287 ::setlocale(::LC_NUMERIC, ''); 288 return $vol; 289} 290 291sub set_equalizer 292{ my (undef,$val)=@_; 293 send_cmd('af','add','@EQ:equalizer='.$val); 294} 295 296sub EQ_Get_Range 297{ return (-12,12,'dB'); 298} 299sub EQ_Get_Hz 300{ my $i=$_[1]; 301 # mplayer and GST equalizers use the same bands, but they are indicated differently 302 # mplayer docs list band center frequences, GST reports band start freqs. Using GST values here for consistency 303 my @bands=(qw/29Hz 59Hz 119Hz 237Hz 474Hz 947Hz 1.9kHz 3.8kHz 7.5kHz 15.0kHz/); 304 return $bands[$i]; 305} 306 307sub get_RG_opts 308{ my $enable = $::Options{use_replaygain} ? 'yes' : 'no'; 309 my $mode = $::Options{rg_albummode} ? 'replaygain-album' : 'replaygain-track'; 310 my $clip = $::Options{rg_limiter} ? 'yes' : 'no'; 311 my $preamp = $::Options{rg_preamp}; 312 #FIXME: enforce limits in interface 313 $preamp = -15 if $::Options{rg_preamp}<-15; 314 $preamp = 15 if $::Options{rg_preamp}>15; 315 my $RGstring = "\@RG:volume=0:$mode=$enable:replaygain-clip=$clip:replaygain-preamp=$preamp"; 316 return $RGstring; 317} 318 319sub RG_set_options 320{ my $RGstring = get_RG_opts(); 321 send_cmd('af', 'add', $RGstring); 322} 323 3241; 325