ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MPV/MPV.pm
(Generate patch)

Comparing AnyEvent-MPV/MPV.pm (file contents):
Revision 1.8 by root, Mon Mar 20 11:12:40 2023 UTC vs.
Revision 1.21 by root, Sun May 26 01:53:22 2024 UTC

3AnyEvent::MPV - remote control mpv (https://mpv.io) 3AnyEvent::MPV - remote control mpv (https://mpv.io)
4 4
5=head1 SYNOPSIS 5=head1 SYNOPSIS
6 6
7 use AnyEvent::MPV; 7 use AnyEvent::MPV;
8
9 my $videofile = "path/to/file.mkv";
10 use AnyEvent;
11 my $mpv = AnyEvent::MPV->new (trace => 1);
12 $mpv->start ("--idle=yes");
13 $mpv->cmd (loadfile => $mpv->escape_binary ($videofile));
14 my $quit = AE::cv;
15 $mpv->register_event (end_file => $quit);
16 $quit->recv;
17
8 18
9=head1 DESCRIPTION 19=head1 DESCRIPTION
10 20
11This module allows you to remote control F<mpv> (a video player). It also 21This module allows you to remote control F<mpv> (a video player). It also
12is an L<AnyEvent> user, you need to make sure that you use and run a 22is an L<AnyEvent> user, you need to make sure that you use and run a
50Here is a very simple client: 60Here is a very simple client:
51 61
52 use AnyEvent; 62 use AnyEvent;
53 use AnyEvent::MPV; 63 use AnyEvent::MPV;
54 64
55 my $videofile = "./xyzzy.mp4"; 65 my $videofile = "./xyzzy.mkv";
56 66
57 my $mpv = AnyEvent::MPV->new (trace => 1); 67 my $mpv = AnyEvent::MPV->new (trace => 1);
58 68
59 $mpv->start ("--", $videofile); 69 $mpv->start ("--", $videofile);
60 70
85shell command), so let us load the file at runtime: 95shell command), so let us load the file at runtime:
86 96
87 use AnyEvent; 97 use AnyEvent;
88 use AnyEvent::MPV; 98 use AnyEvent::MPV;
89 99
90 my $videofile = "./xyzzy.mp4"; 100 my $videofile = "./xyzzy.mkv";
91 101
92 my $mpv = AnyEvent::MPV->new ( 102 my $mpv = AnyEvent::MPV->new (
93 trace => 1, 103 trace => 1,
94 args => ["--pause", "--idle=yes"], 104 args => ["--pause", "--idle=yes"],
95 ); 105 );
140receiving events (using a somewhat embellished example): 150receiving events (using a somewhat embellished example):
141 151
142 use AnyEvent; 152 use AnyEvent;
143 use AnyEvent::MPV; 153 use AnyEvent::MPV;
144 154
145 my $videofile = "xyzzy.mp4"; 155 my $videofile = "xyzzy.mkv";
146 156
147 my $quit = AE::cv; 157 my $quit = AE::cv;
148 158
149 my $mpv = AnyEvent::MPV->new ( 159 my $mpv = AnyEvent::MPV->new (
150 trace => 1, 160 trace => 1,
151 args => ["--pause", "--idle=yes"], 161 args => ["--pause", "--idle=yes"],
152 on_event => sub {
153 my ($mpv, $event, $data) = @_;
154
155 if ($event eq "start-file") {
156 $mpv->cmd ("set", "pause", "no");
157 } elsif ($event eq "end-file") {
158 print "end-file<$data->{reason}>\n";
159 $quit->send;
160 }
161 },
162 ); 162 );
163 163
164 $mpv->start; 164 $mpv->start;
165
166 $mpv->register_event (start_file => sub {
167 $mpv->cmd ("set", "pause", "no");
168 });
169
170 $mpv->register_event (end_file => sub {
171 my ($mpv, $event, $data) = @_;
172
173 print "end-file<$data->{reason}>\n";
174 $quit->send;
175 });
176
165 $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); 177 $mpv->cmd (loadfile => $mpv->escape_binary ($videofile));
166 178
167 $quit->recv; 179 $quit->recv;
168 180
169This example uses a global condvar C<$quit> to wait for the file to finish 181This example uses a global condvar C<$quit> to wait for the file to finish
170playing. Also, most of the logic is now in an C<on_event> callback, which 182playing. Also, most of the logic is now implement in event handlers.
171receives an event name and the actual event object.
172 183
173The two events we handle are C<start-file>, which is emitted by F<mpv> 184The two events handlers we register are C<start-file>, which is emitted by
174once it has loaded a new file, and C<end-file>, which signals the end 185F<mpv> once it has loaded a new file, and C<end-file>, which signals the
175of a file. 186end of a file (underscores are internally replaced by minus signs, so you
187cna speicfy event names with either).
176 188
177In the former event, we again set the C<pause> property to C<no> so the 189In the C<start-file> event, we again set the C<pause> property to C<no>
178movie starts playing. For the latter event, we tell the main program to 190so the movie starts playing. For the C<end-file> event, we tell the main
179quit by invoking C<$quit>. 191program to quit by invoking C<$quit>.
180 192
181This should conclude the basics of operation. There are a few more 193This should conclude the basics of operation. There are a few more
182examples later in the documentation. 194examples later in the documentation.
183 195
184=head2 ENCODING CONVENTIONS 196=head2 ENCODING CONVENTIONS
185 197
186As a rule of thumb, all data you pass to this module to be sent to F<mpv> 198As a rule of thumb, all data you pass to this module to be sent to F<mpv>
187is expected to be in unicode. To pass something that isn't, you need to 199is expected to be in unicode. To pass something that isn't, you need to
188escape it using C<escape_binary>. 200escape it using C<escape_binary>.
189 201
190Data received from C<$mpv>, however, is I<not> decoded to unicode, as data 202Data received from F<mpv>, however, is I<not> decoded to unicode, as data
191returned by F<mpv> is not generally encoded in unicode, and the encoding 203returned by F<mpv> is not generally encoded in unicode, and the encoding
192is usually unspecified. So if you receive data and expect it to be in 204is usually unspecified. So if you receive data and expect it to be in
193unicode, you need to first decode it from UTF-8, but note that this might 205unicode, you need to first decode it from UTF-8, but note that this might
194fail. This is not a limitation of this module - F<mpv> simply does not 206fail. This is not a limitation of this module - F<mpv> simply does not
195specify nor guarantee a specific encoding, or any encoding at all, in its 207specify nor guarantee a specific encoding, or any encoding at all, in its
209use Scalar::Util (); 221use Scalar::Util ();
210 222
211use AnyEvent (); 223use AnyEvent ();
212use AnyEvent::Util (); 224use AnyEvent::Util ();
213 225
226our $VERSION = '1.04';
227
228sub OBSID() { 2**52 }
229
214our $JSON = eval { require JSON::XS; JSON::XS:: } 230our $JSON = eval { require JSON::XS; JSON::XS:: }
215 || do { require JSON::PP; JSON::PP:: }; 231 || do { require JSON::PP; JSON::PP:: };
216 232
217our $JSON_CODER = 233our $JSON_ENCODER = $JSON->new->utf8;
218 234our $JSON_DECODER = $JSON->new->latin1;
219our $VERSION = '0.1';
220 235
221our $mpv_path; # last mpv path used 236our $mpv_path; # last mpv path used
222our $mpv_optionlist; # output of mpv --list-options 237our $mpv_optionlist; # output of mpv --list-options
223 238
224=item $mpv = AnyEvent::MPV->new (key => value...) 239=item $mpv = AnyEvent::MPV->new (key => value...)
381 exit 1; 396 exit 1;
382 } 397 }
383 398
384 $self->{fh} = $fh; 399 $self->{fh} = $fh;
385 400
386 my $trace = delete $self->{trace} || sub { }; 401 my $trace = $self->{trace} || sub { };
387 402
388 $trace = sub { warn "$_[0] $_[1]\n" } if $trace && !ref $trace; 403 $trace = sub { warn "$_[0] $_[1]\n" } if $trace && !ref $trace;
389 404
390 my $buf; 405 my $buf;
391 406
396 while ($buf =~ s/^([^\n]+)\n//) { 411 while ($buf =~ s/^([^\n]+)\n//) {
397 $trace->("mpv>" => "$1"); 412 $trace->("mpv>" => "$1");
398 413
399 if ("{" eq substr $1, 0, 1) { 414 if ("{" eq substr $1, 0, 1) {
400 eval { 415 eval {
401 my $reply = $JSON->new->latin1->decode ($1); 416 my $reply = $JSON_DECODER->decode ($1);
402 417
403 if (exists $reply->{event}) { 418 if (defined (my $event = delete $reply->{event})) {
404 if ( 419 if (
405 $reply->{event} eq "client-message" 420 $event eq "client-message"
406 and $reply->{args}[0] eq "AnyEvent::MPV" 421 and $reply->{args}[0] eq "AnyEvent::MPV"
407 ) { 422 ) {
408 if ($reply->{args}[1] eq "key") { 423 if ($reply->{args}[1] eq "key") {
409 (my $key = $reply->{args}[2]) =~ s/\\x(..)/chr hex $1/ge; 424 (my $key = $reply->{args}[2]) =~ s/\\x(..)/chr hex $1/ge;
410 $self->on_key ($key); 425 $self->on_key ($key);
411 } 426 }
427 } elsif (
428 $event eq "property-change"
429 and OBSID <= $reply->{id}
430 ) {
431 if (my $cb = $self->{obscb}{$reply->{id}}) {
432 $cb->($self, $event, $reply->{data});
433 }
412 } else { 434 } else {
435 if (my $cbs = $self->{evtcb}{$event}) {
436 for my $evtid (keys %$cbs) {
437 my $cb = $cbs->{$evtid}
438 or next;
439 $cb->($self, $event, $reply);
440 }
441 }
442
413 $self->on_event (delete $reply->{event}, $reply); 443 $self->on_event ($event, $reply);
414 } 444 }
415 } elsif (exists $reply->{request_id}) { 445 } elsif (exists $reply->{request_id}) {
416 my $cv = delete $self->{cmd_cv}{$reply->{request_id}}; 446 my $cv = delete $self->{cmdcv}{$reply->{request_id}};
417 447
418 unless ($cv) { 448 unless ($cv) {
419 warn "no cv found for request id <$reply->{request_id}>\n"; 449 warn "no cv found for request id <$reply->{request_id}>\n";
420 next; 450 next;
421 } 451 }
447 my $reqid; 477 my $reqid;
448 478
449 $self->{_cmd} = sub { 479 $self->{_cmd} = sub {
450 my $cv = AE::cv; 480 my $cv = AE::cv;
451 481
452 $self->{cmd_cv}{++$reqid} = $cv; 482 $self->{cmdcv}{++$reqid} = $cv;
453 483
454 my $cmd = $JSON->new->utf8->encode ({ command => ref $_[0] ? $_[0] : \@_, request_id => $reqid*1 }); 484 my $cmd = $JSON_ENCODER->encode ({ command => ref $_[0] ? $_[0] : \@_, request_id => $reqid*1 });
455 485
456 # (un-)apply escape_binary hack 486 # (un-)apply escape_binary hack
457 $cmd =~ s/\xf4\x8e\x97\x9f(..)/sprintf sprintf "\\x%02x", hex $1/ges; # f48e979f == 10e5df in utf-8 487 $cmd =~ s/\xf4\x8e\x97\x9f(..)/sprintf sprintf "\\x%02x", hex $1/ges; # f48e979f == 10e5df in utf-8
458 488
489 $trace->(">mpv" => $cmd);
490
459 $wbuf .= "$cmd\n"; 491 $wbuf .= "$cmd\n";
460 492
461 $trace->(">mpv" => "$_[0]"); 493 my $wcb = sub {
462
463 $self->{ww} ||= AE::io $fh, 1, sub {
464 my $len = syswrite $fh, $wbuf; 494 my $len = syswrite $fh, $wbuf;
465 substr $wbuf, 0, $len, ""; 495 substr $wbuf, 0, $len, "";
466 undef $self->{ww} unless length $wbuf; 496 undef $self->{ww} unless length $wbuf;
467 }; 497 };
468 498
499 $wcb->();
500 $self->{ww} ||= AE::io $fh, 1, $wcb if length $wbuf;
501
469 $cv 502 $cv
470 }; 503 };
471 504
472 1 505 1
473} 506}
491 524
492 if ($self->{pid}) { 525 if ($self->{pid}) {
493 526
494 close delete $self->{fh}; # current mpv versions should cleanup on their own on close 527 close delete $self->{fh}; # current mpv versions should cleanup on their own on close
495 528
529 # clean up zombies, or die trying
530 my $cw; $cw = AE::child $self->{pid}, sub { undef $cw };
531
496 kill TERM => $self->{pid}; 532 kill TERM => $self->{pid};
497 533
498 } 534 }
499 535
500 delete $self->{pid}; 536 delete $self->{pid};
501 delete $self->{cmd_cv}; 537 delete $self->{cmdcv};
538 delete $self->{evtid};
539 delete $self->{evtcb};
502 delete $self->{obsid}; 540 delete $self->{obsid};
541 delete $self->{obscb};
503 delete $self->{wbuf}; 542 delete $self->{wbuf};
504} 543}
505 544
506=item $mpv->on_eof 545=item $mpv->on_eof
507 546
534For subclassing, see I<SUBCLASSING>, below. 573For subclassing, see I<SUBCLASSING>, below.
535 574
536=cut 575=cut
537 576
538sub on_event { 577sub on_event {
539 my ($self, $key) = @_; 578 my ($self, $event, $data) = @_;
540 579
541 $self->{on_event}($self, $key) if $self->{on_event}; 580 $self->{on_event}($self, $event, $data) if $self->{on_event};
542} 581}
543 582
544=item $mpv->on_key ($string) 583=item $mpv->on_key ($string)
545 584
546Invoked when a key declared by C<< ->bind_key >> is pressed. The default 585Invoked when a key declared by C<< ->bind_key >> is pressed. The default
618 &cmd->recv 657 &cmd->recv
619} 658}
620 659
621=item $mpv->bind_key ($INPUT => $string) 660=item $mpv->bind_key ($INPUT => $string)
622 661
623This is an extension implement by this module to make it easy to get key events. The way this is implemented 662This is an extension implement by this module to make it easy to get key
624is to bind a C<client-message> witha first argument of C<AnyEvent::MPV> and the C<$string> you passed. This C<$string> is then 663events. The way this is implemented is to bind a C<client-message> witha
625passed ot the C<on_key> handle when the key is proessed, e.g.: 664first argument of C<AnyEvent::MPV> and the C<$string> you passed. This
665C<$string> is then passed to the C<on_key> handle when the key is
666proessed, e.g.:
626 667
627 my $mpv = AnyEvent::MPV->new ( 668 my $mpv = AnyEvent::MPV->new (
628 on_key => sub { 669 on_key => sub {
629 my ($mpv, $key) = @_; 670 my ($mpv, $key) = @_;
630 671
634 }, 675 },
635 ); 676 );
636 677
637 $mpv_>bind_key (ESC => "letmeout"); 678 $mpv_>bind_key (ESC => "letmeout");
638 679
680You cna find a list of key names L<in the mpv
681documentation|https://mpv.io/manual/stable/#key-names>.
682
639The key configuration is lost when F<mpv> is stopped and must be (re-)done 683The key configuration is lost when F<mpv> is stopped and must be (re-)done
640after every C<start>. 684after every C<start>.
641 685
642=cut 686=cut
643 687
644sub bind_key { 688sub bind_key {
645 my ($self, $key, $event) = @_; 689 my ($self, $key, $event) = @_;
646 690
647 $event =~ s/([^A-Za-z0-9\-_])/sprintf "\\x%02x", ord $1/ge; 691 $event =~ s/([^A-Za-z0-9\-_])/sprintf "\\x%02x", ord $1/ge;
648 $self->cmd (keybind => $key => "no-osd script-message AnyEvent::MPV key $event"); 692 $self->cmd (keybind => $key => "no-osd script-message AnyEvent::MPV key $event");
693}
694
695=item [$guard] = $mpv->register_event ($event => $coderef->($mpv, $event, $data))
696
697This method registers a callback to be invoked for a specific
698event. Whenever the event occurs, it calls the coderef with the C<$mpv>
699object, the C<$event> name and the event object, just like the C<on_event>
700method.
701
702For a lst of events, see L<the mpv
703documentation|https://mpv.io/manual/stable/#list-of-events>. Any
704underscore in the event name is replaced by a minus sign, so you can
705specify event names using underscores for easier quoting in Perl.
706
707In void context, the handler stays registered until C<stop> is called. In
708any other context, it returns a guard object that, when destroyed, will
709unregister the handler.
710
711You can register multiple handlers for the same event, and this method
712does not interfere with the C<on_event> mechanism. That is, you can
713completely ignore this method and handle events in a C<on_event> handler,
714or mix both approaches as you see fit.
715
716Note that unlike commands, event handlers are registered immediately, that
717is, you can issue a command, then register an event handler and then get
718an event for this handler I<before> the command is even sent to F<mpv>. If
719this kind of race is an issue, you can issue a dummy command such as
720C<get_version> and register the handler when the reply is received.
721
722=cut
723
724sub AnyEvent::MPV::Unevent::DESTROY {
725 my ($evtcb, $event, $evtid) = @{$_[0]};
726 delete $evtcb->{$event}{$evtid};
727}
728
729sub register_event {
730 my ($self, $event, $cb) = @_;
731
732 $event =~ y/_/-/;
733
734 my $evtid = ++$self->{evtid};
735 $self->{evtcb}{$event}{$evtid} = $cb;
736
737 defined wantarray
738 and bless [$self->{evtcb}, $event, $evtid], AnyEvent::MPV::Unevent::
739}
740
741=item [$guard] = $mpv->observe_property ($name => $coderef->($mpv, $name, $value))
742
743=item [$guard] = $mpv->observe_property_string ($name => $coderef->($mpv, $name, $value))
744
745These methods wrap a registry system around F<mpv>'s C<observe_property>
746and C<observe_property_string> commands - every time the named property
747changes, the coderef is invoked with the C<$mpv> object, the name of the
748property and the new value.
749
750For a list of properties that you can observe, see L<the mpv
751documentation|https://mpv.io/manual/stable/#property-list>.
752
753Due to the (sane :) way F<mpv> handles these requests, you will always
754get a property cxhange event right after registering an observer (meaning
755you don't have to query the current value), and it is also possible to
756register multiple observers for the same property - they will all be
757handled properly.
758
759When called in void context, the observer stays in place until F<mpv>
760is stopped. In any otrher context, these methods return a guard
761object that, when it goes out of scope, unregisters the observe using
762C<unobserve_property>.
763
764Internally, this method uses observer ids of 2**52 (0x10000000000000) or
765higher - it will not interfere with lower ovserver ids, so it is possible
766to completely ignore this system and execute C<observe_property> commands
767yourself, whilst listening to C<property-change> events - as long as your
768ids stay below 2**52.
769
770Example: register observers for changtes in C<aid> and C<sid>. Note that
771a dummy statement is added to make sure the method is called in void
772context.
773
774 sub register_observers {
775 my ($mpv) = @_;
776
777 $mpv->observe_property (aid => sub {
778 my ($mpv, $name, $value) = @_;
779 print "property aid (=$name) has changed to $value\n";
780 });
781
782 $mpv->observe_property (sid => sub {
783 my ($mpv, $name, $value) = @_;
784 print "property sid (=$name) has changed to $value\n";
785 });
786
787 () # ensure the above method is called in void context
788 }
789
790=cut
791
792sub AnyEvent::MPV::Unobserve::DESTROY {
793 my ($mpv, $obscb, $obsid) = @{$_[0]};
794
795 delete $obscb->{$obsid};
796
797 if ($obscb == $mpv->{obscb}) {
798 $mpv->cmd (unobserve_property => $obsid+0);
799 }
800}
801
802sub _observe_property {
803 my ($self, $type, $property, $cb) = @_;
804
805 my $obsid = OBSID + ++$self->{obsid};
806 $self->cmd ($type => $obsid+0, $property);
807 $self->{obscb}{$obsid} = $cb;
808
809 defined wantarray and do {
810 my $unobserve = bless [$self, $self->{obscb}, $obsid], AnyEvent::MPV::Unobserve::;
811 Scalar::Util::weaken $unobserve->[0];
812 $unobserve
813 }
814}
815
816sub observe_property {
817 my ($self, $property, $cb) = @_;
818
819 $self->_observe_property (observe_property => $property, $cb)
820}
821
822sub observe_property_string {
823 my ($self, $property, $cb) = @_;
824
825 $self->_observe_property (observe_property_string => $property, $cb)
649} 826}
650 827
651=back 828=back
652 829
653=head2 SUBCLASSING 830=head2 SUBCLASSING
661care and deal with the breakage. 838care and deal with the breakage.
662 839
663If you don't want to go to the effort of subclassing this module, you can 840If you don't want to go to the effort of subclassing this module, you can
664also specify all event handlers as constructor keys. 841also specify all event handlers as constructor keys.
665 842
843=head1 EXAMPLES
844
845Here are some real-world code snippets, thrown in here mainly to give you
846some example code to copy.
847
848=head2 doomfrontend
849
850At one point I replaced mythtv-frontend by my own terminal-based video
851player (based on rxvt-unicode). I toyed with the diea of using F<mpv>'s
852subtitle engine to create the user interface, but that is hard to use
853since you don't know how big your letters are. It is also where most of
854this modules code has originally been developed in.
855
856It uses a unified input queue to handle various remote controls, so its
857event handling needs are very simple - it simply feeds all events into the
858input queue:
859
860 my $mpv = AnyEvent::MPV->new (
861 mpv => $MPV,
862 args => \@MPV_ARGS,
863 on_event => sub {
864 input_feed "mpv/$_[1]", $_[2];
865 },
866 on_key => sub {
867 input_feed $_[1];
868 },
869 on_eof => sub {
870 input_feed "mpv/quit";
871 },
872 );
873
874 ...
875
876 $mpv->start ("--idle=yes", "--pause", "--force-window=no");
877
878It also doesn't use complicated command line arguments - the file search
879options have the most impact, as they prevent F<mpv> from scanning
880directories with tens of thousands of files for subtitles and more:
881
882 --audio-client-name=doomfrontend
883 --osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95
884 --sub-auto=exact --audio-file-auto=exact
885
886Since it runs on a TV without a desktop environemnt, it tries to keep complications such as dbus
887away and the screensaver happy:
888
889 # prevent xscreensaver from doing something stupid, such as starting dbus
890 $ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure
891 $ENV{XDG_CURRENT_DESKTOP} = "generic";
892
893It does bind a number of keys to internal (to doomfrontend) commands:
894
895 for (
896 List::Util::pairs qw(
897 ESC return
898 q return
899 ENTER enter
900 SPACE pause
901 [ steprev
902 ] stepfwd
903 j subtitle
904 BS red
905 i green
906 o yellow
907 b blue
908 D triangle
909 UP up
910 DOWN down
911 RIGHT right
912 LEFT left
913 ),
914 (map { ("KP$_" => "num$_") } 0..9),
915 KP_INS => 0, # KP0, but different
916 ) {
917 $mpv->bind_key ($_->[0] => $_->[1]);
918 }
919
920It also reacts to sponsorblock chapters, so it needs to know when vidoe
921chapters change. Preadting C<AnyEvent::MPV>, it handles observers
922manually:
923
924 $mpv->cmd (observe_property => 1, "chapter-metadata");
925
926It also tries to apply an F<mpv> profile, if it exists:
927
928 eval {
929 # the profile is optional
930 $mpv->cmd ("apply-profile" => "doomfrontend");
931 };
932
933Most of the complicated parts deal with saving and restoring per-video
934data, such as bookmarks, playing position, selected audio and subtitle
935tracks and so on. However, since it uses L<Coro>, it can conveniently
936block and wait for replies, which is n ot possible in purely event based
937programs, as you are not allowed to block inside event callbacks in most
938event loops. This simplifies the code quite a bit.
939
940When the file to be played is a Tv recording done by mythtv, it uses the
941C<appending> protocol and deinterlacing:
942
943 if (is_myth $mpv_path) {
944 $mpv_path = "appending://$mpv_path";
945 $initial_deinterlace = 1;
946 }
947
948Otherwise, it sets some defaults and loads the file (I forgot what the
949C<dummy> argument is for, but I am sure it is needed by some F<mpv>
950version):
951
952 $mpv->cmd ("script-message", "osc-visibility", "never", "dummy");
953 $mpv->cmd ("set", "vid", "auto");
954 $mpv->cmd ("set", "aid", "auto");
955 $mpv->cmd ("set", "sid", "no");
956 $mpv->cmd ("set", "file-local-options/chapters-file", $mpv->escape_binary ("$mpv_path.chapters"));
957 $mpv->cmd ("loadfile", $mpv->escape_binary ($mpv_path));
958 $mpv->cmd ("script-message", "osc-visibility", "auto", "dummy");
959
960Handling events makes the main bulk of video playback code. For example,
961various ways of ending playback:
962
963 if ($INPUT eq "mpv/quit") { # should not happen, but allows user to kill etc. without consequence
964 $status = 1;
965 mpv_init; # try reinit
966 last;
967
968 } elsif ($INPUT eq "mpv/idle") { # normal end-of-file
969 last;
970
971 } elsif ($INPUT eq "return") {
972 $status = 1;
973 last;
974
975Or the code that actually starts playback, once the file is loaded:
976
977 our %SAVE_PROPERTY = (aid => 1, sid => 1, "audio-delay" => 1);
978
979 ...
980
981 my $oid = 100;
982
983 } elsif ($INPUT eq "mpv/file-loaded") { # start playing, configure video
984 $mpv->cmd ("seek", $playback_start, "absolute+exact") if $playback_start > 0;
985
986 my $target_fps = eval { $mpv->cmd_recv ("get_property", "container-fps") } || 60;
987 $target_fps *= play_video_speed_mult;
988 set_fps $target_fps;
989
990 unless (eval { $mpv->cmd_recv ("get_property", "video-format") }) {
991 $mpv->cmd ("set", "file-local-options/lavfi-complex", "[aid1] asplit [ao], showcqt=..., format=yuv420p [vo]");
992 };
993
994 for my $prop (keys %SAVE_PROPERTY) {
995 if (exists $PLAYING_STATE->{"mpv_$prop"}) {
996 $mpv->cmd ("set", "$prop", $PLAYING_STATE->{"mpv_$prop"} . "");
997 }
998
999 $mpv->cmd ("observe_property", ++$oid, $prop);
1000 }
1001
1002 play_video_set_speed;
1003 $mpv->cmd ("set", "osd-level", "$OSD_LEVEL");
1004 $mpv->cmd ("observe_property", ++$oid, "osd-level");
1005 $mpv->cmd ("set", "pause", "no");
1006
1007 $mpv->cmd ("set_property", "deinterlace", "yes")
1008 if $initial_deinterlace;
1009
1010There is a lot going on here. First it seeks to the actual playback
1011position, if it is not at the start of the file (it would probaby be more
1012efficient to set the starting position before loading the file, though,
1013but this is good enough).
1014
1015Then it plays with the display fps, to set it to something harmonious
1016w.r.t. the video framerate.
1017
1018If the file does not have a video part, it assumes it is an audio file and
1019sets a visualizer.
1020
1021Also, a number of properties are not global, but per-file. At the moment,
1022this is C<audio-delay>, and the current audio/subtitle track, which it
1023sets, and also creates an observer. Again, this doesn'T use the observe
1024functionality of this module, but handles it itself, assigning obsevrer
1025ids 100+ to temporary/per-file observers.
1026
1027Lastly, it sets some global (or per-youtube-uploader) parameters, such as
1028speed, and unpauses. Property changes are handled like other input events:
1029
1030 } elsif ($INPUT eq "mpv/property-change") {
1031 my $prop = $INPUT_DATA->{name};
1032
1033 if ($prop eq "chapter-metadata") {
1034 if ($INPUT_DATA->{data}{TITLE} =~ /^\[SponsorBlock\]: (.*)/) {
1035 my $section = $1;
1036 my $skip;
1037
1038 $skip ||= $SPONSOR_SKIP{$_}
1039 for split /\s*,\s*/, $section;
1040
1041 if (defined $skip) {
1042 if ($skip) {
1043 # delay a bit, in case we get two metadata changes in quick succession, e.g.
1044 # because we have a skip at file load time.
1045 $skip_delay = AE::timer 2/50, 0, sub {
1046 $mpv->cmd ("no-osd", "add", "chapter", 1);
1047 $mpv->cmd ("show-text", "skipped sponsorblock section \"$section\"", 3000);
1048 };
1049 } else {
1050 undef $skip_delay;
1051 $mpv->cmd ("show-text", "NOT skipping sponsorblock section \"$section\"", 3000);
1052 }
1053 } else {
1054 $mpv->cmd ("show-text", "UNRECOGNIZED sponsorblock section \"$section\"", 60000);
1055 }
1056 } else {
1057 # cancel a queued skip
1058 undef $skip_delay;
1059 }
1060
1061 } elsif (exists $SAVE_PROPERTY{$prop}) {
1062 $PLAYING_STATE->{"mpv_$prop"} = $INPUT_DATA->{data};
1063 ::state_save;
1064 }
1065
1066This saves back the per-file properties, and also handles chapter changes
1067in a hacky way.
1068
1069Most of the handlers are very simple, though. For example:
1070
1071 } elsif ($INPUT eq "pause") {
1072 $mpv->cmd ("cycle", "pause");
1073 $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
1074 } elsif ($INPUT eq "right") {
1075 $mpv->cmd ("osd-msg-bar", "seek", 30, "relative+exact");
1076 } elsif ($INPUT eq "left") {
1077 $mpv->cmd ("osd-msg-bar", "seek", -5, "relative+exact");
1078 } elsif ($INPUT eq "up") {
1079 $mpv->cmd ("osd-msg-bar", "seek", +600, "relative+exact");
1080 } elsif ($INPUT eq "down") {
1081 $mpv->cmd ("osd-msg-bar", "seek", -600, "relative+exact");
1082 } elsif ($INPUT eq "select") {
1083 $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "-0.100");
1084 } elsif ($INPUT eq "start") {
1085 $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "0.100");
1086 } elsif ($INPUT eq "intfwd") {
1087 $mpv->cmd ("no-osd", "frame-step");
1088 } elsif ($INPUT eq "audio") {
1089 $mpv->cmd ("osd-auto", "cycle", "audio");
1090 } elsif ($INPUT eq "subtitle") {
1091 $mpv->cmd ("osd-auto", "cycle", "sub");
1092 } elsif ($INPUT eq "triangle") {
1093 $mpv->cmd ("osd-auto", "cycle", "deinterlace");
1094
1095Once a file has finished playing (or the user strops playback), it pauses,
1096unobserves the per-file observers, and saves the current position for to
1097be able to resume:
1098
1099 $mpv->cmd ("set", "pause", "yes");
1100
1101 while ($oid > 100) {
1102 $mpv->cmd ("unobserve_property", $oid--);
1103 }
1104
1105 $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
1106
1107And thats most of the F<mpv>-related code.
1108
1109=head2 F<Gtk2::CV>
1110
1111F<Gtk2::CV> is low-feature image viewer that I use many times daily
1112because it can handle directories with millions of files without falling
1113over. It also had the ability to play videos for ages, but it used an
1114older, crappier protocol to talk to F<mpv> and used F<ffprobe> before
1115playing each file instead of letting F<mpv> handle format/size detection.
1116
1117After writing this module, I decided to upgprade Gtk2::CV by making use
1118of it, with the goal of getting rid of F<ffprobe> and being ablew to
1119reuse F<mpv> processes, which would have a multitude of speed benefits
1120(for example, fork+exec of F<mpv> caused the kernel to close all file
1121descriptors, which could take minutes if a large file was being copied via
1122NFS, as the kernel waited for thr buffers to be flushed on close - not
1123having to start F<mpv> gets rid of this issue).
1124
1125Setting up is only complicated by the fact that F<mpv> needs to be
1126embedded into an existing window. To keep control of all inputs,
1127F<Gtk2::CV> puts an eventbox in front of F<mpv>, so F<mpv> receives no
1128input events:
1129
1130 $self->{mpv} = AnyEvent::MPV->new (
1131 trace => $ENV{CV_MPV_TRACE},
1132 );
1133
1134 # create an eventbox, so we receive all input events
1135 my $box = $self->{mpv_eventbox} = new Gtk2::EventBox;
1136 $box->set_above_child (1);
1137 $box->set_visible_window (0);
1138 $box->set_events ([]);
1139 $box->can_focus (0);
1140
1141 # create a drawingarea that mpv can display into
1142 my $window = $self->{mpv_window} = new Gtk2::DrawingArea;
1143 $box->add ($window);
1144
1145 # put the drawingarea intot he eventbox, and the eventbox into our display window
1146 $self->add ($box);
1147
1148 # we need to pass the window id to F<mpv>, which means we need to realise
1149 # the drawingarea, so an X window is allocated for it.
1150 $self->show_all;
1151 $window->realize;
1152 my $xid = $window->window->get_xid;
1153
1154Then it starts F<mpv> using this setup:
1155
1156 local $ENV{LC_ALL} = "POSIX";
1157 $self->{mpv}->start (
1158 "--no-terminal",
1159 "--no-input-terminal",
1160 "--no-input-default-bindings",
1161 "--no-input-cursor",
1162 "--input-conf=/dev/null",
1163 "--input-vo-keyboard=no",
1164
1165 "--loop-file=inf",
1166 "--force-window=yes",
1167 "--idle=yes",
1168
1169 "--audio-client-name=CV",
1170
1171 "--osc=yes", # --osc=no displays fading play/pause buttons instead
1172
1173 "--wid=$xid",
1174 );
1175
1176 $self->{mpv}->cmd ("script-message" => "osc-visibility" => "never", "dummy");
1177 $self->{mpv}->cmd ("osc-idlescreen" => "no");
1178
1179It also prepares a hack to force a ConfigureNotify event on every vidoe
1180reconfig:
1181
1182 # force a configurenotify on every video-reconfig
1183 $self->{mpv_reconfig} = $self->{mpv}->register_event (video_reconfig => sub {
1184 my ($mpv, $event, $data) = @_;
1185
1186 $self->mpv_window_update;
1187 });
1188
1189The way this is done is by doing a "dummy" resize to 1x1 and back:
1190
1191 $self->{mpv_window}->window->resize (1, 1),
1192 $self->{mpv_window}->window->resize ($self->{w}, $self->{h});
1193
1194Without this, F<mpv> often doesn't "get" the correct window size. Doing
1195it this way is not nice, but I didn't fine a nicer way to do it.
1196
1197When no file is being played, F<mpv> is hidden and prepared:
1198
1199 $self->{mpv_eventbox}->hide;
1200
1201 $self->{mpv}->cmd (set_property => "pause" => "yes");
1202 $self->{mpv}->cmd ("playlist_remove", "current");
1203 $self->{mpv}->cmd (set_property => "video-rotate" => 0);
1204 $self->{mpv}->cmd (set_property => "lavfi-complex" => "");
1205
1206Loading a file is a bit more complicated, as bluray and DVD rips are
1207supported:
1208
1209 if ($moviedir) {
1210 if ($moviedir eq "br") {
1211 $mpv->cmd (set => "bluray-device" => $path);
1212 $mpv->cmd (loadfile => "bd://");
1213 } elsif ($moviedir eq "dvd") {
1214 $mpv->cmd (set => "dvd-device" => $path);
1215 $mpv->cmd (loadfile => "dvd://");
1216 }
1217 } elsif ($type eq "video/iso-bluray") {
1218 $mpv->cmd (set => "bluray-device" => $path);
1219 $mpv->cmd (loadfile => "bd://");
1220 } else {
1221 $mpv->cmd (loadfile => $mpv->escape_binary ($path));
1222 }
1223
1224After this, C<Gtk2::CV> waits for the file to be loaded, video to be
1225configured, and then queries the video size (to resize its own window)
1226and video format (to decide whether an audio visualizer is needed for
1227audio playback). The problematic word here is "wait", as this needs to be
1228imploemented using callbacks.
1229
1230This made the code much harder to write, as the whole setup is very
1231asynchronous (C<Gtk2::CV> talks to the command interface in F<mpv>, which
1232talks to the decode and playback parts, all of which run asynchronously
1233w.r.t. each other. In practise, this can mean that C<Gtk2::CV> waits for
1234a file to be loaded by F<mpv> while the command interface of F<mpv> still
1235deals with the previous file and the decoder still handles an even older
1236file). Adding to this fact is that Gtk2::CV is bound by the glib event
1237loop, which means we cannot wait for replies form F<mpv> anywhere, so
1238everything has to be chained callbacks.
1239
1240The way this is handled is by creating a new empty hash ref that is unique
1241for each loaded file, and use it to detect whether the event is old or
1242not, and also store C<AnyEvent::MPV> guard objects in it:
1243
1244 # every time we loaded a file, we create a new hash
1245 my $guards = $self->{mpv_guards} = { };
1246
1247Then, when we wait for an event to occur, delete the handler, and, if the
1248C<mpv_guards> object has changed, we ignore it. Something like this:
1249
1250 $guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
1251 delete $guards->{file_loaded};
1252 return if $guards != $self->{mpv_guards};
1253
1254Commands do not have guards since they cnanot be cancelled, so we don't
1255have to do this for commands. But what prevents us form misinterpreting
1256an old event? Since F<mpv> (by default) handles commands synchronously,
1257we can queue a dummy command, whose only purpose is to tell us when all
1258previous commands are done. We use C<get_version> for this.
1259
1260The simplified code looks like this:
1261
1262 Scalar::Util::weaken $self;
1263
1264 $mpv->cmd ("get_version")->cb (sub {
1265
1266 $guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
1267 delete $guards->{file_loaded};
1268 return if $guards != $self->{mpv_guards};
1269
1270 $mpv->cmd (get_property => "video-format")->cb (sub {
1271 return if $guards != $self->{mpv_guards};
1272
1273 # video-format handling
1274 return if eval { $_[0]->recv; 1 };
1275
1276 # no video? assume audio and visualize, cpu usage be damned
1277 $mpv->cmd (set => "lavfi-complex" => ...");
1278 });
1279
1280 $guards->{show} = $mpv->register_event (video_reconfig => sub {
1281 delete $guards->{show};
1282 return if $guards != $self->{mpv_guards};
1283
1284 $self->{mpv_eventbox}->show_all;
1285
1286 $w = $mpv->cmd (get_property => "dwidth");
1287 $h = $mpv->cmd (get_property => "dheight");
1288
1289 $h->cb (sub {
1290 $w = eval { $w->recv };
1291 $h = eval { $h->recv };
1292
1293 $mpv->cmd (set_property => "pause" => "no");
1294
1295 if ($w && $h) {
1296 # resize our window
1297 }
1298
1299 });
1300 });
1301
1302 });
1303
1304 });
1305
1306Most of the rest of the code is much simpler and just deals with forwarding user commands:
1307
1308 } elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10");
1309 } elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10");
1310 } elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60");
1311 } elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60");
1312 } elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio");
1313 } elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub");
1314 } elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2");
1315 } elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause");
1316 } elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2");
1317 } elsif ($key == $Gtk2::Gdk::Keysyms{0} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "+2");
1318
666=head1 SEE ALSO 1319=head1 SEE ALSO
667 1320
668L<AnyEvent>, L<the mpv command documentation|https://mpv.io/manual/stable/#command-interface>. 1321L<AnyEvent>, L<the mpv command documentation|https://mpv.io/manual/stable/#command-interface>.
669 1322
670=head1 AUTHOR 1323=head1 AUTHOR

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines