AnyEvent-MPV

 view release on metacpan or  search on metacpan

MPV.pm  view on Meta::CPAN

=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.pm  view on Meta::CPAN

   my ($class, %kv) = @_;

   bless {
      mpv     => "mpv",
      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");

            if ("{" eq substr $1, 0, 1) {
               eval {
                  my $reply = $JSON_DECODER->decode ($1);

MPV.pm  view on Meta::CPAN

   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
unregister the handler.

You can register multiple handlers for the same event, and this method
does not interfere with the C<on_event> mechanism. That is, you can
completely ignore this method and handle events in a C<on_event> handler,
or mix both approaches as you see fit.

Note that unlike commands, event handlers are registered immediately, that
is, you can issue a command, then register an event handler and then get
an event for this handler I<before> the command is even sent to F<mpv>. If
this kind of race is an issue, you can issue a dummy command such as
C<get_version> and register the handler when the reply is received.

=cut

sub AnyEvent::MPV::Unevent::DESTROY {
   my ($evtcb, $event, $evtid) = @{$_[0]};
   delete $evtcb->{$event}{$evtid};
}

sub register_event {
   my ($self, $event, $cb) = @_;

   $event =~ y/_/-/;

   my $evtid = ++$self->{evtid};
   $self->{evtcb}{$event}{$evtid} = $cb;

   defined wantarray
      and bless [$self->{evtcb}, $event, $evtid], AnyEvent::MPV::Unevent::
}

=item [$guard] = $mpv->observe_property ($name => $coderef->($mpv, $name, $value))

=item [$guard] = $mpv->observe_property_string ($name => $coderef->($mpv, $name, $value))

These methods wrap a registry system around F<mpv>'s C<observe_property>
and C<observe_property_string> commands - every time the named property
changes, the coderef is invoked with the C<$mpv> object, the name of the
property and the new value.

For a list of properties that you can observe, see L<the mpv
documentation|https://mpv.io/manual/stable/#property-list>.

MPV.pm  view on Meta::CPAN

hashes, with the constructor simply storing all passed key-value pairs in
the object. If you want to subclass to provide your own C<on_*> methods,
be my guest and rummage around in the internals as much as you wish - the
only guarantee that this module dcoes is that it will not use keys with
double colons in the name, so youc an use those, or chose to simply not
care and deal with the breakage.

If you don't want to go to the effort of subclassing this module, you can
also specify all event handlers as constructor keys.

=head1 EXAMPLES

Here are some real-world code snippets, thrown in here mainly to give you
some example code to copy.

=head2 doomfrontend

At one point I replaced mythtv-frontend by my own terminal-based video
player (based on rxvt-unicode). I toyed with the diea of using F<mpv>'s
subtitle engine to create the user interface, but that is hard to use
since you don't know how big your letters are. It is also where most of
this modules code has originally been developed in.

It uses a unified input queue to handle various remote controls, so its
event handling needs are very simple - it simply feeds all events into the
input queue:

   my $mpv = AnyEvent::MPV->new (
      mpv   => $MPV,
      args  => \@MPV_ARGS,
      on_event => sub {
	 input_feed "mpv/$_[1]", $_[2];
      },
      on_key => sub {
	 input_feed $_[1];
      },
      on_eof => sub {
	 input_feed "mpv/quit";
      },
   );

   ...

   $mpv->start ("--idle=yes", "--pause", "--force-window=no");

It also doesn't use complicated command line arguments - the file search
options have the most impact, as they prevent F<mpv> from scanning
directories with tens of thousands of files for subtitles and more:

   --audio-client-name=doomfrontend
   --osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95
   --sub-auto=exact --audio-file-auto=exact

Since it runs on a TV without a desktop environemnt, it tries to keep complications such as dbus
away and the screensaver happy:

   # prevent xscreensaver from doing something stupid, such as starting dbus
   $ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure
   $ENV{XDG_CURRENT_DESKTOP} = "generic";

It does bind a number of keys to internal (to doomfrontend) commands:

   for (
      List::Util::pairs qw(
         ESC   return
         q     return
         ENTER enter
         SPACE pause
         [     steprev
         ]     stepfwd
         j     subtitle
         BS    red
         i     green
         o     yellow
         b     blue
         D     triangle
         UP    up
         DOWN  down
         RIGHT right
         LEFT  left
      ),
      (map { ("KP$_" => "num$_") } 0..9),
      KP_INS => 0, # KP0, but different
   ) {
      $mpv->bind_key ($_->[0] => $_->[1]);
   }

It also reacts to sponsorblock chapters, so it needs to know when vidoe
chapters change. Preadting C<AnyEvent::MPV>, it handles observers
manually:

   $mpv->cmd (observe_property => 1, "chapter-metadata");

It also tries to apply an F<mpv> profile, if it exists:

   eval {
      # the profile is optional
      $mpv->cmd ("apply-profile" => "doomfrontend");
   };

Most of the complicated parts deal with saving and restoring per-video
data, such as bookmarks, playing position, selected audio and subtitle
tracks and so on. However, since it uses L<Coro>, it can conveniently
block and wait for replies, which is n ot possible in purely event based
programs, as you are not allowed to block inside event callbacks in most
event loops. This simplifies the code quite a bit.

When the file to be played is a Tv recording done by mythtv, it uses the
C<appending> protocol and deinterlacing:

   if (is_myth $mpv_path) {
      $mpv_path = "appending://$mpv_path";
      $initial_deinterlace = 1;
   }

Otherwise, it sets some defaults and loads the file (I forgot what the
C<dummy> argument is for, but I am sure it is needed by some F<mpv>
version):

   $mpv->cmd ("script-message", "osc-visibility", "never", "dummy");
   $mpv->cmd ("set", "vid", "auto");
   $mpv->cmd ("set", "aid", "auto");
   $mpv->cmd ("set", "sid", "no");
   $mpv->cmd ("set", "file-local-options/chapters-file", $mpv->escape_binary ("$mpv_path.chapters"));
   $mpv->cmd ("loadfile", $mpv->escape_binary ($mpv_path));
   $mpv->cmd ("script-message", "osc-visibility", "auto", "dummy");

Handling events makes the main bulk of video playback code. For example,
various ways of ending playback:

      if ($INPUT eq "mpv/quit") { # should not happen, but allows user to kill etc. without consequence
         $status = 1;
         mpv_init; # try reinit
         last;

      } elsif ($INPUT eq "mpv/idle") { # normal end-of-file
         last;

      } elsif ($INPUT eq "return") {
         $status = 1;
         last;

Or the code that actually starts playback, once the file is loaded:

   our %SAVE_PROPERTY = (aid => 1, sid => 1, "audio-delay" => 1);

MPV.pm  view on Meta::CPAN


   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 {



( run in 0.466 second using v1.01-cache-2.11-cpan-2398b32b56e )