AnyEvent-MPV
view release on metacpan or search on metacpan
=item F<--wid=id>, to embed F<mpv> into another application.
=item F<--no-terminal>, F<--no-input-default-bindings>, F<--no-input-cursor>, F<--input-conf=/dev/null>, F<--input-vo-keyboard=no> - to ensure only you control input.
=back
The return value can be used to decide whether F<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 F<mpv> has not yet started.
=cut
sub start {
my ($self, @extra_args) = @_;
return 0 if $self->{fh};
# cache optionlist for same "path"
($mpv_path, $mpv_optionlist) = ($self->{mpv}, scalar qx{\Q$self->{mpv}\E --list-options})
if $self->{mpv} ne $mpv_path;
my $options = $mpv_optionlist;
my ($fh, $slave) = AnyEvent::Util::portable_socketpair
or die "socketpair: $!\n";
AnyEvent::Util::fh_nonblocking $fh, 1;
$self->{pid} = fork;
if ($self->{pid} eq 0) {
AnyEvent::Util::fh_nonblocking $slave, 0;
fcntl $slave, Fcntl::F_SETFD, 0;
my $input_file = $options =~ /\s--input-ipc-client\s/ ? "input-ipc-client" : "input-file";
exec $self->{mpv},
qw(--no-input-terminal),
($self->{trace} ? "--quiet" : "--really-quiet"),
"--$input_file=fd://" . (fileno $slave),
@{ $self->{args} },
@extra_args;
exit 1;
}
$self->{fh} = $fh;
my $trace = $self->{trace} || sub { };
$trace = sub { warn "$_[0] $_[1]\n" } if $trace && !ref $trace;
my $buf;
Scalar::Util::weaken $self;
$self->{rw} = AE::io $fh, 0, sub {
if (sysread $fh, $buf, 8192, length $buf) {
while ($buf =~ s/^([^\n]+)\n//) {
$trace->("mpv>" => "$1");
if ("{" eq substr $1, 0, 1) {
eval {
my $reply = $JSON_DECODER->decode ($1);
if (defined (my $event = delete $reply->{event})) {
if (
$event eq "client-message"
and $reply->{args}[0] eq "AnyEvent::MPV"
) {
if ($reply->{args}[1] eq "key") {
(my $key = $reply->{args}[2]) =~ s/\\x(..)/chr hex $1/ge;
$self->on_key ($key);
}
} elsif (
$event eq "property-change"
and OBSID <= $reply->{id}
) {
if (my $cb = $self->{obscb}{$reply->{id}}) {
$cb->($self, $event, $reply->{data});
}
} else {
if (my $cbs = $self->{evtcb}{$event}) {
for my $evtid (keys %$cbs) {
my $cb = $cbs->{$evtid}
or next;
$cb->($self, $event, $reply);
}
}
$self->on_event ($event, $reply);
}
} elsif (exists $reply->{request_id}) {
my $cv = delete $self->{cmdcv}{$reply->{request_id}};
unless ($cv) {
warn "no cv found for request id <$reply->{request_id}>\n";
next;
}
if (exists $reply->{data}) {
$cv->send ($reply->{data});
} elsif ($reply->{error} eq "success") { # success means error... eh.. no...
$cv->send;
} else {
$cv->croak ($reply->{error});
}
} else {
warn "unexpected reply from mpv, pleasew report: <$1>\n";
}
};
warn $@ if $@;
} else {
$trace->("mpv>" => "$1");
documentation|https://mpv.io/manual/stable/#property-list>.
Due to the (sane :) way F<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 F<mpv>
is stopped. In any otrher context, these methods return a guard
object that, when it goes out of scope, unregisters the observe using
C<unobserve_property>.
Internally, this method uses observer ids of 2**52 (0x10000000000000) or
higher - it will not interfere with lower ovserver ids, so it is possible
to completely ignore this system and execute C<observe_property> commands
yourself, whilst listening to C<property-change> events - as long as your
ids stay below 2**52.
Example: register observers for changtes in C<aid> and C<sid>. Note that
a dummy statement is added to make sure the method is called in void
context.
sub register_observers {
my ($mpv) = @_;
$mpv->observe_property (aid => sub {
my ($mpv, $name, $value) = @_;
print "property aid (=$name) has changed to $value\n";
});
$mpv->observe_property (sid => sub {
my ($mpv, $name, $value) = @_;
print "property sid (=$name) has changed to $value\n";
});
() # ensure the above method is called in void context
}
=cut
sub AnyEvent::MPV::Unobserve::DESTROY {
my ($mpv, $obscb, $obsid) = @{$_[0]};
delete $obscb->{$obsid};
if ($obscb == $mpv->{obscb}) {
$mpv->cmd (unobserve_property => $obsid+0);
}
}
sub _observe_property {
my ($self, $type, $property, $cb) = @_;
my $obsid = OBSID + ++$self->{obsid};
$self->cmd ($type => $obsid+0, $property);
$self->{obscb}{$obsid} = $cb;
defined wantarray and do {
my $unobserve = bless [$self, $self->{obscb}, $obsid], AnyEvent::MPV::Unobserve::;
Scalar::Util::weaken $unobserve->[0];
$unobserve
}
}
sub observe_property {
my ($self, $property, $cb) = @_;
$self->_observe_property (observe_property => $property, $cb)
}
sub observe_property_string {
my ($self, $property, $cb) = @_;
$self->_observe_property (observe_property_string => $property, $cb)
}
=back
=head2 SUBCLASSING
Like most perl objects, C<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 C<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.
=head1 EXAMPLES
Here are some real-world code snippets, thrown in here mainly to give you
some example code to copy.
=head2 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 F<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";
},
$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, C<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 (C<Gtk2::CV> talks to the command interface in F<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 C<Gtk2::CV> waits for
a file to be loaded by F<mpv> while the command interface of F<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 F<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 C<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
C<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 F<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 C<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
}
});
});
});
});
Most of the rest of the code is much simpler and just deals with forwarding user commands:
} elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10");
} elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10");
} elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60");
} elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60");
} elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio");
} elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub");
} elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2");
} elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause");
} elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2");
} elsif ($key == $Gtk2::Gdk::Keysyms{0} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "+2");
=head1 SEE ALSO
L<AnyEvent>, L<the mpv command documentation|https://mpv.io/manual/stable/#command-interface>.
( run in 2.011 seconds using v1.01-cache-2.11-cpan-5837b0d9d2c )