AnyEvent-MPV
view release on metacpan or search on metacpan
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);
...
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"} . "");
}
"--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");
$h = $mpv->cmd (get_property => "dheight");
$h->cb (sub {
$w = eval { $w->recv };
$h = eval { $h->recv };
$mpv->cmd (set_property => "pause" => "no");
if ($w && $h) {
# resize our window
}
( run in 1.207 second using v1.01-cache-2.11-cpan-39bf76dae61 )