AnyEvent-MPV
view release on metacpan or search on metacpan
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");
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
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.
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
object that, when it goes out of scope, unregisters the observe
using "unobserve_property".
Internally, this method uses observer ids of 2**52
(0x10000000000000) or higher - it will not interfere with lower
SUBCLASSING
Like most perl objects, "AnyEvent::MPV" objects are implemented as
hashes, with the constructor simply storing all passed key-value pairs
in the object. If you want to subclass to provide your own "on_*"
methods, be my guest and rummage around in the internals as much as you
wish - the only guarantee that this module dcoes is that it will not use
keys with double colons in the name, so youc an use those, or chose to
simply not care and deal with the breakage.
If you don't want to go to the effort of subclassing this module, you
can also specify all event handlers as constructor keys.
EXAMPLES
Here are some real-world code snippets, thrown in here mainly to give
you some example code to copy.
doomfrontend
At one point I replaced mythtv-frontend by my own terminal-based video
player (based on rxvt-unicode). I toyed with the diea of using mpv's
subtitle engine to create the user interface, but that is hard to use
since you don't know how big your letters are. It is also where most of
this modules code has originally been developed in.
It uses a unified input queue to handle various remote controls, so its
event handling needs are very simple - it simply feeds all events into
the input queue:
my $mpv = AnyEvent::MPV->new (
mpv => $MPV,
args => \@MPV_ARGS,
on_event => sub {
input_feed "mpv/$_[1]", $_[2];
},
on_key => sub {
input_feed $_[1];
},
on_eof => sub {
input_feed "mpv/quit";
},
);
...
$mpv->start ("--idle=yes", "--pause", "--force-window=no");
It also doesn't use complicated command line arguments - the file search
options have the most impact, as they prevent mpv from scanning
directories with tens of thousands of files for subtitles and more:
--audio-client-name=doomfrontend
--osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95
--sub-auto=exact --audio-file-auto=exact
Since it runs on a TV without a desktop environemnt, it tries to keep
complications such as dbus away and the screensaver happy:
# prevent xscreensaver from doing something stupid, such as starting dbus
$ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure
$ENV{XDG_CURRENT_DESKTOP} = "generic";
It does bind a number of keys to internal (to doomfrontend) commands:
for (
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);
$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 {
( run in 0.905 second using v1.01-cache-2.11-cpan-2398b32b56e )