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

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines