AnyEvent-MPV

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

        The first/implicit argument is the $mpv object, the second is the
        event name (same as "$data->{event}", purely for convenience), and
        the third argument is the event object as sent by mpv (sans "event"
        key). See List of events
        <https://mpv.io/manual/stable/#list-of-events> in its documentation.

        For subclassing, see *SUBCLASSING*, below.

    $mpv->on_key ($string)
        Invoked when a key declared by "->bind_key" is pressed. The default
        invokes the "on_key" code reference specified in the constructor
        with the $mpv object and the key name as arguments, or do nothing if
        none was given.

        For more details and examples, see the "bind_key" method.

        For subclassing, see *SUBCLASSING*, below.

    $mpv->cmd ($command => $arg, $arg...)
        Queues a command to be sent to mpv, using the given arguments, and
        immediately return a condvar.

        See 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 "recv" is called.

    $result = $mpv->cmd_recv ($command => $arg, $arg...)
        The same as calling "cmd" and immediately "recv" on its return
        value. Useful when you don't want to mess with mpv asynchronously or
        simply needs to have the result:

           $mpv->cmd_recv ("stop");
           $position = $mpv->cmd_recv ("get_property", "playback-time");

    $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
        "client-message" witha first argument of "AnyEvent::MPV" and the
        $string you passed. This $string is then passed to the "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 in the mpv documentation
        <https://mpv.io/manual/stable/#key-names>.

        The key configuration is lost when mpv is stopped and must be
        (re-)done after every "start".

    [$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 $mpv
        object, the $event name and the event object, just like the
        "on_event" method.

        For a lst of events, see 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 "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 "on_event" mechanism. That is,
        you can completely ignore this method and handle events in a
        "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 *before* the
        command is even sent to mpv. If this kind of race is an issue, you
        can issue a dummy command such as "get_version" and register the
        handler when the reply is received.

    [$guard] = $mpv->observe_property ($name => $coderef->($mpv, $name,
    $value))
    [$guard] = $mpv->observe_property_string ($name => $coderef->($mpv,
    $name, $value))
        These methods wrap a registry system around mpv's "observe_property"
        and "observe_property_string" commands - every time the named
        property changes, the coderef is invoked with the $mpv object, the
        name of the property and the new value.

README  view on Meta::CPAN

             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 "AnyEvent::MPV", it handles observers
    manually:

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

    It also tries to apply an 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 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
    "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
    "dummy" argument is for, but I am sure it is needed by some 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);
   
       ...

       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.
       $self->show_all;
       $window->realize;
       my $xid = $window->window->get_xid;

    Then it starts mpv using this setup:



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