AnyEvent-MPV

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

NAME
    AnyEvent::MPV - remote control mpv (https://mpv.io)

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;

DESCRIPTION
    This module allows you to remote control mpv (a video player). It also
    is an 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 mpv command interface
    <https://mpv.io/manual/stable/#command-interface> is required to use
    this module.

    Features of this module are:

    uses AnyEvent, so integrates well into most event-based programs
    supports asynchronous and synchronous operation
    allows you to properly pass binary filenames
    accepts data encoded in any way (does not crash when mpv replies with
    non UTF-8 data)
    features a simple keybind/event system

  OVERVIEW OF OPERATION
    This module forks an mpv process and uses --input-ipc-client (or
    equivalent) to create a bidirectional communication channel between it
    and the mpv process.

    It then speaks the somewhat JSON-looking (but not really being JSON)
    protocol that 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 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.

  ENCODING CONVENTIONS
    As a rule of thumb, all data you pass to this module to be sent to mpv
    is expected to be in unicode. To pass something that isn't, you need to
    escape it using "escape_binary".

    Data received from mpv, however, is *not* decoded to unicode, as data
    returned by mpv is not generally encoded in unicode, and the encoding is
    usually unspecified. So if you receive data and expect it to be in
    unicode, you need to first decode it from UTF-8, but note that this
    might fail. This is not a limitation of this module - mpv simply does
    not specify nor guarantee a specific encoding, or any encoding at all,
    in its protocol.

  METHODS
    $mpv = AnyEvent::MPV->new (key => value...)
        Creates a new "mpv" object, but does not yet do anything. The
        support key-value pairs are:

        mpv => $path
            The path to the mpv binary to use - by default, "mpv" is used
            and therefore, uses your "PATH" to find it.

        args => [...]
            Arguments to pass to mpv. These arguments are passed after the
            hardcoded arguments used by this module, but before the
            arguments passed ot "start". It does not matter whether you
            specify your arguments using this key, or in the "start" call,
            but when you invoke mpv multiple times, typically the arguments
            used for all invocations go here, while arguments used for
            specific invocations (e..g filenames) are passed to "start".

        trace => false|true|coderef
            Enables tracing if true. In trace mode, output from mpv is
            printed to standard error using a "mpv>" prefix, and commands
            sent to mpv are printed with a ">mpv" prefix.

            If a code reference is passed, then instead of printing to
            standard errort, this coderef is invoked with a first arfgument
            being either "mpv>" or ">mpv", and the second argument being a
            string to display. The default implementation simply does this:

               sub {
                  warn "$_[0] $_[1]\n";
               }

        on_eof => $coderef->($mpv)
        on_event => $coderef->($mpv, $event, $data)
        on_key => $coderef->($mpv, $string)
            These are invoked by the default method implementation of the
            same name - see below.

    $string = $mpv->escape_binary ($string)
        This module excects all command data sent to 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 "loadfile" command:

           $mpv->cmd_recv (loadfile => $mpv->escape_binary ($path));

    $started = $mpv->start (argument...)
        Starts mpv, passing the given arguemnts as extra arguments to mpv.
        If mpv is already running, it returns false, otherwise it returns a
        true value, so you can easily start mpv on demand by calling "start"
        just before using it, and if it is already running, it will not be
        started again.

        The arguments passwd to 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 --no-input-terminal, --really-quiet (or
        --quiet in "trace" mode), and "--input-ipc-client" (or equivalent).

        Some commonly used and/or even useful arguments you might want to
        pass are:

        --idle=yes or --idle=once to keep mpv from quitting when you don't
        specify a file to play.
        --pause, to keep mpv from instantly starting to play a file, in case
        you want to inspect/change properties first.
        --force-window=no (or similar), to keep mpv from instantly opening a
        window, or to force it to do so.
        --audio-client-name=yourappname, to make sure audio streams are
        associated witht eh right program.
        --wid=id, to embed mpv into another application.
        --no-terminal, --no-input-default-bindings, --no-input-cursor,
        --input-conf=/dev/null, --input-vo-keyboard=no - to ensure only you
        control input.

        The return value can be used to decide whether 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 mpv has not yet started.

    $mpv->stop
        Ensures that mpv is being stopped, by killing mpv with a "TERM"
        signal if needed. After this, you can "->start" a new instance
        again.

    $mpv->on_eof
        This method is called when mpv quits - usually unexpectedly. The
        default implementation will call the "on_eof" code reference
        specified in the constructor, or do nothing if none was given.

        For subclassing, see *SUBCLASSING*, below.

    $mpv->on_event ($event, $data)
        This method is called when mpv sends an asynchronous event. The
        default implementation will call the "on_event" code reference
        specified in the constructor, or do nothing if none was given.

        The first/implicit argument is the $mpv object, the second is the

README  view on Meta::CPAN

        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.

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

        Due to the (sane :) way mpv handles these requests, you will always
        get a property cxhange event right after registering an observer
        (meaning you don't have to query the current value), and it is also
        possible to register multiple observers for the same property - they
        will all be handled properly.

        When called in void context, the observer stays in place until mpv
        is stopped. In any otrher context, these methods return a guard

README  view on Meta::CPAN

          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 "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.

README  view on Meta::CPAN

          "--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, 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, 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, "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 ("Gtk2::CV" talks to the command interface in 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 "Gtk2::CV" waits for
    a file to be loaded by mpv while the command interface of 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 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 "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 "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 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 "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};



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