AnyEvent-MPV
view release on metacpan or search on metacpan
}
=item $mpv->cmd ($command => $arg, $arg...)
Queues a command to be sent to F<mpv>, using the given arguments, and
immediately return a condvar.
See L<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 C<recv> is called.
=cut
sub cmd {
my $self = shift;
$self->{_cmd}->(@_)
}
=item $result = $mpv->cmd_recv ($command => $arg, $arg...)
The same as calling C<cmd> and immediately C<recv> on its return
value. Useful when you don't want to mess with F<mpv> asynchronously or
simply needs to have the result:
$mpv->cmd_recv ("stop");
$position = $mpv->cmd_recv ("get_property", "playback-time");
=cut
sub cmd_recv {
&cmd->recv
}
=item $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 C<client-message> witha
first argument of C<AnyEvent::MPV> and the C<$string> you passed. This
C<$string> is then passed to the C<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 L<in the mpv
documentation|https://mpv.io/manual/stable/#key-names>.
The key configuration is lost when F<mpv> is stopped and must be (re-)done
after every C<start>.
=cut
sub bind_key {
my ($self, $key, $event) = @_;
$event =~ s/([^A-Za-z0-9\-_])/sprintf "\\x%02x", ord $1/ge;
$self->cmd (keybind => $key => "no-osd script-message AnyEvent::MPV key $event");
}
=item [$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 C<$mpv>
object, the C<$event> name and the event object, just like the C<on_event>
method.
For a lst of events, see L<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 C<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 C<on_event> mechanism. That is, you can
completely ignore this method and handle events in a C<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 I<before> the command is even sent to F<mpv>. If
this kind of race is an issue, you can issue a dummy command such as
C<get_version> and register the handler when the reply is received.
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 C<AnyEvent::MPV>, it handles observers
manually:
$mpv->cmd (observe_property => 1, "chapter-metadata");
It also tries to apply an F<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 L<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
C<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
C<dummy> argument is for, but I am sure it is needed by some F<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 C<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 F<mpv>-related code.
=head2 F<Gtk2::CV>
F<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 F<mpv> and used F<ffprobe> before
playing each file instead of letting F<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 F<ffprobe> and being ablew to
reuse F<mpv> processes, which would have a multitude of speed benefits
(for example, fork+exec of F<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 F<mpv> gets rid of this issue).
Setting up is only complicated by the fact that F<mpv> needs to be
embedded into an existing window. To keep control of all inputs,
F<Gtk2::CV> puts an eventbox in front of F<mpv>, so F<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;
( run in 0.563 second using v1.01-cache-2.11-cpan-13bb782fe5a )