AnyEvent-MPV

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

    This starts mpv with the two arguments "--" and $videofile, which it
    should load and play. It then waits two seconds by starting a timer and
    quits. The "trace" argument to the constructor makes 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 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 "->start" mpv, while the arguments to "->start" are
    only used for this specific clal to0 "start". The argument --pause keeps
    mpv in pause mode (i.e. it does not play the file after loading it), and
    "--idle=yes" tells 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 "loadfile" command, which accepts, as
    first argument, the URL or path to a video file. To make sure mpv does
    not misinterpret the path as a URL, it was prefixed with ./ (similarly
    to "protecting" paths in perls "open").

    Since commands send *to* mpv are send in UTF-8, we need to escape the
    filename (which might be in any encoding) using the "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 "cmd_recv" method then queues the command, waits for a reply and
    returns the reply data (or croaks on error). mpv would, at this point,
    load the file and, if everything was successful, show the first frame
    and pause. Note that, since 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 "loadfile" command itself will run successfully.

    To unpause, we send another command, "set", to set the "pause" property
    to "no", this time using the "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 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 $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 "start-file", which is emitted
    by mpv once it has loaded a new file, and "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 "start-file" event, we again set the "pause" property to "no" so
    the movie starts playing. For the "end-file" event, we tell the main
    program to quit by invoking $quit.

    This should conclude the basics of operation. There are a few more
    examples later in the documentation.

README  view on Meta::CPAN

       $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);
   
       ...

       my $oid = 100;

          } elsif ($INPUT eq "mpv/file-loaded") { # start playing, configure video
             $mpv->cmd ("seek", $playback_start, "absolute+exact") if $playback_start > 0;

             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 "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");

    And thats most of the mpv-related code.

  Gtk2::CV
    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 mpv and used ffprobe before playing each
    file instead of letting 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 ffprobe and being ablew to reuse
    mpv processes, which would have a multitude of speed benefits (for
    example, fork+exec of 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 mpv gets rid of this issue).

    Setting up is only complicated by the fact that mpv needs to be embedded
    into an existing window. To keep control of all inputs, Gtk2::CV puts an
    eventbox in front of mpv, so 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.



( run in 1.319 second using v1.01-cache-2.11-cpan-df04353d9ac )