AnyEvent-MPV
view release on metacpan or search on metacpan
=head1 NAME
AnyEvent::MPV - remote control mpv (https://mpv.io)
=head1 SYNOPSIS
use AnyEvent::MPV;
my $videofile = "path/to/file.mkv";
use AnyEvent;
my $mpv = AnyEvent::MPV->new (trace => 1);
$mpv->start ("--idle=yes");
$mpv->cmd (loadfile => $mpv->escape_binary ($videofile));
my $quit = AE::cv;
$mpv->register_event (end_file => $quit);
$quit->recv;
=head1 DESCRIPTION
This module allows you to remote control F<mpv> (a video player). It also
is an L<AnyEvent> user, you need to make sure that you use and run a
supported event loop.
There are other modules doing this, and I haven't looked much at them
other than to decide that they don't handle encodings correctly, and since
none of them use AnyEvent, I wrote my own. When in doubt, have a look at
them, too.
Knowledge of the L<mpv command
interface|https://mpv.io/manual/stable/#command-interface> is required to
use this module.
Features of this module are:
=over
=item uses AnyEvent, so integrates well into most event-based programs
=item supports asynchronous and synchronous operation
=item allows you to properly pass binary filenames
=item accepts data encoded in any way (does not crash when mpv replies with non UTF-8 data)
=item features a simple keybind/event system
=back
=head2 OVERVIEW OF OPERATION
This module forks an F<mpv> process and uses F<--input-ipc-client> (or
equivalent) to create a bidirectional communication channel between it and
the F<mpv> process.
It then speaks the somewhat JSON-looking (but not really being JSON)
protocol that F<mpv> implements to both send it commands, decode and
handle replies, and handle asynchronous events.
Here is a very simple client:
use AnyEvent;
use AnyEvent::MPV;
my $videofile = "./xyzzy.mkv";
my $mpv = AnyEvent::MPV->new (trace => 1);
$mpv->start ("--", $videofile);
my $timer = AE::timer 2, 0, my $quit = AE::cv;
$quit->recv;
This starts F<mpv> with the two arguments C<--> and C<$videofile>, which
it should load and play. It then waits two seconds by starting a timer and
quits. The C<trace> argument to the constructor makes F<mpv> more verbose
and also prints the commands and responses, so you can have an idea what
is going on.
In my case, the above example would output something like this:
[uosc] Disabled because original osc is enabled!
mpv> {"event":"start-file","playlist_entry_id":1}
mpv> {"event":"tracks-changed"}
(+) Video --vid=1 (*) (h264 480x480 30.000fps)
mpv> {"event":"metadata-update"}
mpv> {"event":"file-loaded"}
Using hardware decoding (nvdec).
mpv> {"event":"video-reconfig"}
VO: [gpu] 480x480 cuda[nv12]
mpv> {"event":"video-reconfig"}
mpv> {"event":"playback-restart"}
This is not usually very useful (you could just run F<mpv> as a simple
shell command), so let us load the file at runtime:
use AnyEvent;
use AnyEvent::MPV;
my $videofile = "./xyzzy.mkv";
my $mpv = AnyEvent::MPV->new (
trace => 1,
args => ["--pause", "--idle=yes"],
);
$mpv->start;
$mpv->cmd_recv (loadfile => $mpv->escape_binary ($videofile));
$mpv->cmd ("set", "pause", "no");
my $timer = AE::timer 2, 0, my $quit = AE::cv;
$quit->recv;
This specifies extra arguments in the constructor - these arguments are
used every time you C<< ->start >> F<mpv>, while the arguments to C<<
->start >> are only used for this specific clal to0 C<start>. The argument
F<--pause> keeps F<mpv> in pause mode (i.e. it does not play the file
after loading it), and C<--idle=yes> tells F<mpv> to not quit when it does
not have a playlist - as no files are specified on the command line.
To load a file, we then send it a C<loadfile> command, which accepts, as
first argument, the URL or path to a video file. To make sure F<mpv> does
not misinterpret the path as a URL, it was prefixed with F<./> (similarly
to "protecting" paths in perls C<open>).
Since commands send I<to> F<mpv> are send in UTF-8, we need to escape the
filename (which might be in any encoding) using the C<esscape_binary>
method - this is not needed if your filenames are just ascii, or magically
get interpreted correctly, but if you accept arbitrary filenamews (e.g.
from the user), you need to do this.
The C<cmd_recv> method then queues the command, waits for a reply and
returns the reply data (or croaks on error). F<mpv> would, at this point,
load the file and, if everything was successful, show the first frame and
pause. Note that, since F<mpv> is implement rather synchronously itself,
do not expect commands to fail in many circumstances - for example, fit
he file does not exit, you will likely get an event, but the C<loadfile>
command itself will run successfully.
To unpause, we send another command, C<set>, to set the C<pause> property
to C<no>, this time using the C<cmd> method, which queues the command, but
instead of waiting for a reply, it immediately returns a condvar that cna
be used to receive results.
This should then cause F<mpv> to start playing the video.
It then again waits two seconds and quits.
Now, just waiting two seconds is rather, eh, unuseful, so let's look at
receiving events (using a somewhat embellished example):
use AnyEvent;
use AnyEvent::MPV;
my $videofile = "xyzzy.mkv";
my $quit = AE::cv;
my $mpv = AnyEvent::MPV->new (
trace => 1,
args => ["--pause", "--idle=yes"],
);
$mpv->start;
$mpv->register_event (start_file => sub {
$mpv->cmd ("set", "pause", "no");
});
$mpv->register_event (end_file => sub {
my ($mpv, $event, $data) = @_;
print "end-file<$data->{reason}>\n";
$quit->send;
});
$mpv->cmd (loadfile => $mpv->escape_binary ($videofile));
$quit->recv;
This example uses a global condvar C<$quit> to wait for the file to finish
playing. Also, most of the logic is now implement in event handlers.
The two events handlers we register are C<start-file>, which is emitted by
F<mpv> once it has loaded a new file, and C<end-file>, which signals the
end of a file (underscores are internally replaced by minus signs, so you
cna speicfy event names with either).
In the C<start-file> event, we again set the C<pause> property to C<no>
so the movie starts playing. For the C<end-file> event, we tell the main
program to quit by invoking C<$quit>.
This should conclude the basics of operation. There are a few more
examples later in the documentation.
$self->{ww} ||= AE::io $fh, 1, $wcb if length $wbuf;
$cv
};
1
}
sub DESTROY {
$_[0]->stop;
}
=item $mpv->stop
Ensures that F<mpv> is being stopped, by killing F<mpv> with a C<TERM>
signal if needed. After this, you can C<< ->start >> a new instance again.
=cut
sub stop {
my ($self) = @_;
delete $self->{rw};
delete $self->{ww};
if ($self->{pid}) {
close delete $self->{fh}; # current mpv versions should cleanup on their own on close
kill TERM => $self->{pid};
}
delete $self->{pid};
delete $self->{cmdcv};
delete $self->{evtid};
delete $self->{evtcb};
delete $self->{obsid};
delete $self->{obscb};
delete $self->{wbuf};
}
=item $mpv->on_eof
This method is called when F<mpv> quits - usually unexpectedly. The
default implementation will call the C<on_eof> code reference specified in
the constructor, or do nothing if none was given.
For subclassing, see I<SUBCLASSING>, below.
=cut
sub on_eof {
my ($self) = @_;
$self->{on_eof}($self) if $self->{on_eof};
}
=item $mpv->on_event ($event, $data)
This method is called when F<mpv> sends an asynchronous event. The default
implementation will call the C<on_event> code reference specified in the
constructor, or do nothing if none was given.
The first/implicit argument is the C<$mpv> object, the second is the
event name (same as C<< $data->{event} >>, purely for convenience), and
the third argument is the event object as sent by F<mpv> (sans C<event>
key). See L<List of events|https://mpv.io/manual/stable/#list-of-events>
in its documentation.
For subclassing, see I<SUBCLASSING>, below.
=cut
sub on_event {
my ($self, $event, $data) = @_;
$self->{on_event}($self, $event, $data) if $self->{on_event};
}
=item $mpv->on_key ($string)
Invoked when a key declared by C<< ->bind_key >> is pressed. The default
invokes the C<on_key> code reference specified in the constructor with the
C<$mpv> object and the key name as arguments, or do nothing if none was
given.
For more details and examples, see the C<bind_key> method.
For subclassing, see I<SUBCLASSING>, below.
=cut
sub on_key {
my ($self, $key) = @_;
$self->{on_key}($self, $key) if $self->{on_key};
}
=item $mpv->cmd ($command => $arg, $arg...)
Queues a command to be sent to F<mpv>, using the given arguments, and
immediately return a condvar.
See L<the mpv
documentation|https://mpv.io/manual/stable/#list-of-input-commands> for
details on individual commands.
The condvar can be ignored:
$mpv->cmd (set_property => "deinterlace", "yes");
Or it can be used to synchronously wait for the command results:
$cv = $mpv->cmd (get_property => "video-format");
$format = $cv->recv;
# or simpler:
$format = $mpv->cmd (get_property => "video-format")->recv;
# or even simpler:
$format = $mpv->cmd_recv (get_property => "video-format");
Or you can set a callback:
$cv = $mpv->cmd (get_property => "video-format");
$cv->cb (sub {
my $format = $_[0]->recv;
});
On error, the condvar will croak when C<recv> is called.
=cut
sub cmd {
my $self = shift;
$self->{_cmd}->(@_)
}
=item $result = $mpv->cmd_recv ($command => $arg, $arg...)
The same as calling C<cmd> and immediately C<recv> on its return
value. Useful when you don't want to mess with F<mpv> asynchronously or
simply needs to have the result:
$mpv->cmd_recv ("stop");
$position = $mpv->cmd_recv ("get_property", "playback-time");
=cut
sub cmd_recv {
&cmd->recv
}
=item $mpv->bind_key ($INPUT => $string)
This is an extension implement by this module to make it easy to get key
events. The way this is implemented is to bind a C<client-message> witha
first argument of C<AnyEvent::MPV> and the C<$string> you passed. This
C<$string> is then passed to the C<on_key> handle when the key is
proessed, e.g.:
my $mpv = AnyEvent::MPV->new (
on_key => sub {
my ($mpv, $key) = @_;
if ($key eq "letmeout") {
print "user pressed escape\n";
}
},
);
$mpv_>bind_key (ESC => "letmeout");
You cna find a list of key names L<in the mpv
documentation|https://mpv.io/manual/stable/#key-names>.
The key configuration is lost when F<mpv> is stopped and must be (re-)done
after every C<start>.
=cut
sub bind_key {
my ($self, $key, $event) = @_;
$event =~ s/([^A-Za-z0-9\-_])/sprintf "\\x%02x", ord $1/ge;
$self->cmd (keybind => $key => "no-osd script-message AnyEvent::MPV key $event");
}
=item [$guard] = $mpv->register_event ($event => $coderef->($mpv, $event, $data))
This method registers a callback to be invoked for a specific
event. Whenever the event occurs, it calls the coderef with the C<$mpv>
object, the C<$event> name and the event object, just like the C<on_event>
method.
For a lst of events, see L<the mpv
documentation|https://mpv.io/manual/stable/#list-of-events>. Any
underscore in the event name is replaced by a minus sign, so you can
specify event names using underscores for easier quoting in Perl.
In void context, the handler stays registered until C<stop> is called. In
any other context, it returns a guard object that, when destroyed, will
"--osc=yes", # --osc=no displays fading play/pause buttons instead
"--wid=$xid",
);
$self->{mpv}->cmd ("script-message" => "osc-visibility" => "never", "dummy");
$self->{mpv}->cmd ("osc-idlescreen" => "no");
It also prepares a hack to force a ConfigureNotify event on every vidoe
reconfig:
# force a configurenotify on every video-reconfig
$self->{mpv_reconfig} = $self->{mpv}->register_event (video_reconfig => sub {
my ($mpv, $event, $data) = @_;
$self->mpv_window_update;
});
The way this is done is by doing a "dummy" resize to 1x1 and back:
$self->{mpv_window}->window->resize (1, 1),
$self->{mpv_window}->window->resize ($self->{w}, $self->{h});
Without this, F<mpv> often doesn't "get" the correct window size. Doing
it this way is not nice, but I didn't fine a nicer way to do it.
When no file is being played, F<mpv> is hidden and prepared:
$self->{mpv_eventbox}->hide;
$self->{mpv}->cmd (set_property => "pause" => "yes");
$self->{mpv}->cmd ("playlist_remove", "current");
$self->{mpv}->cmd (set_property => "video-rotate" => 0);
$self->{mpv}->cmd (set_property => "lavfi-complex" => "");
Loading a file is a bit more complicated, as bluray and DVD rips are
supported:
if ($moviedir) {
if ($moviedir eq "br") {
$mpv->cmd (set => "bluray-device" => $path);
$mpv->cmd (loadfile => "bd://");
} elsif ($moviedir eq "dvd") {
$mpv->cmd (set => "dvd-device" => $path);
$mpv->cmd (loadfile => "dvd://");
}
} elsif ($type eq "video/iso-bluray") {
$mpv->cmd (set => "bluray-device" => $path);
$mpv->cmd (loadfile => "bd://");
} else {
$mpv->cmd (loadfile => $mpv->escape_binary ($path));
}
After this, C<Gtk2::CV> waits for the file to be loaded, video to be
configured, and then queries the video size (to resize its own window)
and video format (to decide whether an audio visualizer is needed for
audio playback). The problematic word here is "wait", as this needs to be
imploemented using callbacks.
This made the code much harder to write, as the whole setup is very
asynchronous (C<Gtk2::CV> talks to the command interface in F<mpv>, which
talks to the decode and playback parts, all of which run asynchronously
w.r.t. each other. In practise, this can mean that C<Gtk2::CV> waits for
a file to be loaded by F<mpv> while the command interface of F<mpv> still
deals with the previous file and the decoder still handles an even older
file). Adding to this fact is that Gtk2::CV is bound by the glib event
loop, which means we cannot wait for replies form F<mpv> anywhere, so
everything has to be chained callbacks.
The way this is handled is by creating a new empty hash ref that is unique
for each loaded file, and use it to detect whether the event is old or
not, and also store C<AnyEvent::MPV> guard objects in it:
# every time we loaded a file, we create a new hash
my $guards = $self->{mpv_guards} = { };
Then, when we wait for an event to occur, delete the handler, and, if the
C<mpv_guards> object has changed, we ignore it. Something like this:
$guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
delete $guards->{file_loaded};
return if $guards != $self->{mpv_guards};
Commands do not have guards since they cnanot be cancelled, so we don't
have to do this for commands. But what prevents us form misinterpreting
an old event? Since F<mpv> (by default) handles commands synchronously,
we can queue a dummy command, whose only purpose is to tell us when all
previous commands are done. We use C<get_version> for this.
The simplified code looks like this:
Scalar::Util::weaken $self;
$mpv->cmd ("get_version")->cb (sub {
$guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
delete $guards->{file_loaded};
return if $guards != $self->{mpv_guards};
$mpv->cmd (get_property => "video-format")->cb (sub {
return if $guards != $self->{mpv_guards};
# video-format handling
return if eval { $_[0]->recv; 1 };
# no video? assume audio and visualize, cpu usage be damned
$mpv->cmd (set => "lavfi-complex" => ...");
});
$guards->{show} = $mpv->register_event (video_reconfig => sub {
delete $guards->{show};
return if $guards != $self->{mpv_guards};
$self->{mpv_eventbox}->show_all;
$w = $mpv->cmd (get_property => "dwidth");
$h = $mpv->cmd (get_property => "dheight");
$h->cb (sub {
$w = eval { $w->recv };
$h = eval { $h->recv };
$mpv->cmd (set_property => "pause" => "no");
if ($w && $h) {
# resize our window
}
});
});
});
});
Most of the rest of the code is much simpler and just deals with forwarding user commands:
} elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10");
} elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10");
} elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60");
} elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60");
} elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio");
} elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub");
} elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2");
} elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause");
} elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2");
( run in 1.413 second using v1.01-cache-2.11-cpan-9581c071862 )