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

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines