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"}
args => [],
%kv,
}, $class
}
=item $string = $mpv->escape_binary ($string)
This module excects all command data sent to F<mpv> to be in unicode. Some
things are not, such as filenames. To pass binary data such as filenames
through a comamnd, you need to escape it using this method.
The simplest example is a C<loadfile> command:
$mpv->cmd_recv (loadfile => $mpv->escape_binary ($path));
=cut
# can be used to escape filenames
sub escape_binary {
shift;
local $_ = shift;
# we escape every "illegal" octet using U+10e5df HEX. this is later undone in cmd
s/([\x00-\x1f\x80-\xff])/sprintf "\x{10e5df}%02x", ord $1/ge;
$_
}
=item $started = $mpv->start (argument...)
Starts F<mpv>, passing the given arguemnts as extra arguments to
F<mpv>. If F<mpv> is already running, it returns false, otherwise it
returns a true value, so you can easily start F<mpv> on demand by calling
C<start> just before using it, and if it is already running, it will not
be started again.
The arguments passwd to F<mpv> are a set of hardcoded built-in arguments,
followed by the arguments specified in the constructor, followed by the
arguments passwd to this method. The built-in arguments currently are
F<--no-input-terminal>, F<--really-quiet> (or F<--quiet> in C<trace>
mode), and C<--input-ipc-client> (or equivalent).
Some commonly used and/or even useful arguments you might want to pass are:
=over
=item F<--idle=yes> or F<--idle=once> to keep F<mpv> from quitting when you
don't specify a file to play.
=item F<--pause>, to keep F<mpv> from instantly starting to play a file, in case you want to
inspect/change properties first.
=item F<--force-window=no> (or similar), to keep F<mpv> from instantly opening a window, or to force it to do so.
=item F<--audio-client-name=yourappname>, to make sure audio streams are associated witht eh right program.
=item F<--wid=id>, to embed F<mpv> into another application.
=item F<--no-terminal>, F<--no-input-default-bindings>, F<--no-input-cursor>, F<--input-conf=/dev/null>, F<--input-vo-keyboard=no> - to ensure only you control input.
=back
The return value can be used to decide whether F<mpv> needs initializing:
if ($mpv->start) {
$mpv->bind_key (...);
$mpv->cmd (set => property => value);
...
}
You can immediately starting sending commands when this method returns,
even if F<mpv> has not yet started.
=cut
sub start {
my ($self, @extra_args) = @_;
return 0 if $self->{fh};
# cache optionlist for same "path"
($mpv_path, $mpv_optionlist) = ($self->{mpv}, scalar qx{\Q$self->{mpv}\E --list-options})
if $self->{mpv} ne $mpv_path;
my $options = $mpv_optionlist;
my ($fh, $slave) = AnyEvent::Util::portable_socketpair
or die "socketpair: $!\n";
AnyEvent::Util::fh_nonblocking $fh, 1;
$self->{pid} = fork;
if ($self->{pid} eq 0) {
AnyEvent::Util::fh_nonblocking $slave, 0;
fcntl $slave, Fcntl::F_SETFD, 0;
my $input_file = $options =~ /\s--input-ipc-client\s/ ? "input-ipc-client" : "input-file";
exec $self->{mpv},
qw(--no-input-terminal),
($self->{trace} ? "--quiet" : "--really-quiet"),
"--$input_file=fd://" . (fileno $slave),
@{ $self->{args} },
@extra_args;
exit 1;
}
$self->{fh} = $fh;
my $trace = $self->{trace} || sub { };
$trace = sub { warn "$_[0] $_[1]\n" } if $trace && !ref $trace;
my $buf;
Scalar::Util::weaken $self;
$self->{rw} = AE::io $fh, 0, sub {
if (sysread $fh, $buf, 8192, length $buf) {
while ($buf =~ s/^([^\n]+)\n//) {
$trace->("mpv>" => "$1");
# cancel a queued skip
undef $skip_delay;
}
} elsif (exists $SAVE_PROPERTY{$prop}) {
$PLAYING_STATE->{"mpv_$prop"} = $INPUT_DATA->{data};
::state_save;
}
This saves back the per-file properties, and also handles chapter changes
in a hacky way.
Most of the handlers are very simple, though. For example:
} elsif ($INPUT eq "pause") {
$mpv->cmd ("cycle", "pause");
$PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
} elsif ($INPUT eq "right") {
$mpv->cmd ("osd-msg-bar", "seek", 30, "relative+exact");
} elsif ($INPUT eq "left") {
$mpv->cmd ("osd-msg-bar", "seek", -5, "relative+exact");
} elsif ($INPUT eq "up") {
$mpv->cmd ("osd-msg-bar", "seek", +600, "relative+exact");
} elsif ($INPUT eq "down") {
$mpv->cmd ("osd-msg-bar", "seek", -600, "relative+exact");
} elsif ($INPUT eq "select") {
$mpv->cmd ("osd-msg-bar", "add", "audio-delay", "-0.100");
} elsif ($INPUT eq "start") {
$mpv->cmd ("osd-msg-bar", "add", "audio-delay", "0.100");
} elsif ($INPUT eq "intfwd") {
$mpv->cmd ("no-osd", "frame-step");
} elsif ($INPUT eq "audio") {
$mpv->cmd ("osd-auto", "cycle", "audio");
} elsif ($INPUT eq "subtitle") {
$mpv->cmd ("osd-auto", "cycle", "sub");
} elsif ($INPUT eq "triangle") {
$mpv->cmd ("osd-auto", "cycle", "deinterlace");
Once a file has finished playing (or the user strops playback), it pauses,
unobserves the per-file observers, and saves the current position for to
be able to resume:
$mpv->cmd ("set", "pause", "yes");
while ($oid > 100) {
$mpv->cmd ("unobserve_property", $oid--);
}
$PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
And thats most of the F<mpv>-related code.
=head2 F<Gtk2::CV>
F<Gtk2::CV> is low-feature image viewer that I use many times daily
because it can handle directories with millions of files without falling
over. It also had the ability to play videos for ages, but it used an
older, crappier protocol to talk to F<mpv> and used F<ffprobe> before
playing each file instead of letting F<mpv> handle format/size detection.
After writing this module, I decided to upgprade Gtk2::CV by making use
of it, with the goal of getting rid of F<ffprobe> and being ablew to
reuse F<mpv> processes, which would have a multitude of speed benefits
(for example, fork+exec of F<mpv> caused the kernel to close all file
descriptors, which could take minutes if a large file was being copied via
NFS, as the kernel waited for thr buffers to be flushed on close - not
having to start F<mpv> gets rid of this issue).
Setting up is only complicated by the fact that F<mpv> needs to be
embedded into an existing window. To keep control of all inputs,
F<Gtk2::CV> puts an eventbox in front of F<mpv>, so F<mpv> receives no
input events:
$self->{mpv} = AnyEvent::MPV->new (
trace => $ENV{CV_MPV_TRACE},
);
# create an eventbox, so we receive all input events
my $box = $self->{mpv_eventbox} = new Gtk2::EventBox;
$box->set_above_child (1);
$box->set_visible_window (0);
$box->set_events ([]);
$box->can_focus (0);
# create a drawingarea that mpv can display into
my $window = $self->{mpv_window} = new Gtk2::DrawingArea;
$box->add ($window);
# put the drawingarea intot he eventbox, and the eventbox into our display window
$self->add ($box);
# we need to pass the window id to F<mpv>, which means we need to realise
# the drawingarea, so an X window is allocated for it.
$self->show_all;
$window->realize;
my $xid = $window->window->get_xid;
Then it starts F<mpv> using this setup:
local $ENV{LC_ALL} = "POSIX";
$self->{mpv}->start (
"--no-terminal",
"--no-input-terminal",
"--no-input-default-bindings",
"--no-input-cursor",
"--input-conf=/dev/null",
"--input-vo-keyboard=no",
"--loop-file=inf",
"--force-window=yes",
"--idle=yes",
"--audio-client-name=CV",
"--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");
( run in 1.120 second using v1.01-cache-2.11-cpan-2398b32b56e )