AnyEvent-MPV
view release on metacpan or search on metacpan
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.
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 )