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