AnyEvent-MPV

 view release on metacpan or  search on metacpan

MPV.pm  view on Meta::CPAN

   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) = @_;

MPV.pm  view on Meta::CPAN


         my $target_fps = eval { $mpv->cmd_recv ("get_property", "container-fps") } || 60;
         $target_fps *= play_video_speed_mult;
         set_fps $target_fps;

         unless (eval { $mpv->cmd_recv ("get_property", "video-format") }) {
            $mpv->cmd ("set", "file-local-options/lavfi-complex", "[aid1] asplit [ao], showcqt=..., format=yuv420p [vo]");
         };

         for my $prop (keys %SAVE_PROPERTY) {
            if (exists $PLAYING_STATE->{"mpv_$prop"}) {
               $mpv->cmd ("set", "$prop", $PLAYING_STATE->{"mpv_$prop"} . "");
            }

            $mpv->cmd ("observe_property", ++$oid, $prop);
         }

         play_video_set_speed;
         $mpv->cmd ("set", "osd-level", "$OSD_LEVEL");
         $mpv->cmd ("observe_property", ++$oid, "osd-level");
         $mpv->cmd ("set", "pause", "no");

         $mpv->cmd ("set_property", "deinterlace", "yes")
            if $initial_deinterlace;

There is a lot going on here. First it seeks to the actual playback
position, if it is not at the start of the file (it would probaby be more
efficient to set the starting position before loading the file, though,
but this is good enough).

Then it plays with the display fps, to set it to something harmonious
w.r.t. the video framerate.

If the file does not have a video part, it assumes it is an audio file and
sets a visualizer.

Also, a number of properties are not global, but per-file. At the moment,
this is C<audio-delay>, and the current audio/subtitle track, which it
sets, and also creates an observer. Again, this doesn'T use the observe
functionality of this module, but handles it itself, assigning obsevrer
ids 100+ to temporary/per-file observers.

Lastly, it sets some global (or per-youtube-uploader) parameters, such as
speed, and unpauses. Property changes are handled like other input events:

      } elsif ($INPUT eq "mpv/property-change") {
         my $prop = $INPUT_DATA->{name};

         if ($prop eq "chapter-metadata") {
            if ($INPUT_DATA->{data}{TITLE} =~ /^\[SponsorBlock\]: (.*)/) {
               my $section = $1;
               my $skip;

               $skip ||= $SPONSOR_SKIP{$_}
                  for split /\s*,\s*/, $section;

               if (defined $skip) {
                  if ($skip) {
                     # delay a bit, in case we get two metadata changes in quick succession, e.g.
                     # because we have a skip at file load time.
                     $skip_delay = AE::timer 2/50, 0, sub {
                        $mpv->cmd ("no-osd", "add", "chapter", 1);
                        $mpv->cmd ("show-text", "skipped sponsorblock section \"$section\"", 3000);
                     };
                  } else {
                     undef $skip_delay;
                     $mpv->cmd ("show-text", "NOT skipping sponsorblock section \"$section\"", 3000);
                  }
               } else {
                  $mpv->cmd ("show-text", "UNRECOGNIZED sponsorblock section \"$section\"", 60000);
               }
            } else {
               # 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");



( run in 0.952 second using v1.01-cache-2.11-cpan-39bf76dae61 )