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

README  view on Meta::CPAN


        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
        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:

README  view on Meta::CPAN

                } 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:

       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, 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};
                return if $guards != $self->{mpv_guards};

                $self->{mpv_eventbox}->show_all;

                $w = $mpv->cmd (get_property => "dwidth");



( run in 1.126 second using v1.01-cache-2.11-cpan-5837b0d9d2c )