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.15 by root, Wed Mar 22 01:00:36 2023 UTC vs.
Revision 1.16 by root, Wed Mar 22 18:17:24 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;
13 $mpv->cmd (loadfile => $mpv->escape_binary ($videofile));
14 my $quit = AE::cv;
15 $mpv->register_event (end_file => $cv);
16 $quit->recv;
8 17
9=head1 DESCRIPTION 18=head1 DESCRIPTION
10 19
11This module allows you to remote control F<mpv> (a video player). It also 20This 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 21is an L<AnyEvent> user, you need to make sure that you use and run a
50Here is a very simple client: 59Here is a very simple client:
51 60
52 use AnyEvent; 61 use AnyEvent;
53 use AnyEvent::MPV; 62 use AnyEvent::MPV;
54 63
55 my $videofile = "./xyzzy.mp4"; 64 my $videofile = "./xyzzy.mkv";
56 65
57 my $mpv = AnyEvent::MPV->new (trace => 1); 66 my $mpv = AnyEvent::MPV->new (trace => 1);
58 67
59 $mpv->start ("--", $videofile); 68 $mpv->start ("--", $videofile);
60 69
85shell command), so let us load the file at runtime: 94shell command), so let us load the file at runtime:
86 95
87 use AnyEvent; 96 use AnyEvent;
88 use AnyEvent::MPV; 97 use AnyEvent::MPV;
89 98
90 my $videofile = "./xyzzy.mp4"; 99 my $videofile = "./xyzzy.mkv";
91 100
92 my $mpv = AnyEvent::MPV->new ( 101 my $mpv = AnyEvent::MPV->new (
93 trace => 1, 102 trace => 1,
94 args => ["--pause", "--idle=yes"], 103 args => ["--pause", "--idle=yes"],
95 ); 104 );
140receiving events (using a somewhat embellished example): 149receiving events (using a somewhat embellished example):
141 150
142 use AnyEvent; 151 use AnyEvent;
143 use AnyEvent::MPV; 152 use AnyEvent::MPV;
144 153
145 my $videofile = "xyzzy.mp4"; 154 my $videofile = "xyzzy.mkv";
146 155
147 my $quit = AE::cv; 156 my $quit = AE::cv;
148 157
149 my $mpv = AnyEvent::MPV->new ( 158 my $mpv = AnyEvent::MPV->new (
150 trace => 1, 159 trace => 1,
822care and deal with the breakage. 831care and deal with the breakage.
823 832
824If you don't want to go to the effort of subclassing this module, you can 833If you don't want to go to the effort of subclassing this module, you can
825also specify all event handlers as constructor keys. 834also specify all event handlers as constructor keys.
826 835
836=head1 EXAMPLES
837
838Here are some real-world code snippets, thrown in here mainly to give you
839some example code to copy.
840
841=head2 doomfrontend
842
843At one point I replaced mythtv-frontend by my own terminal-based video
844player (based on rxvt-unicode). I toyed with the diea of using F<mpv>'s
845subtitle engine to create the user interface, but that is hard to use
846since you don't know how big your letters are. It is also where most of
847this modules code has originally been developed in.
848
849It uses a unified input queue to handle various remote controls, so its
850event handling needs are very simple - it simply feeds all events into the
851input queue:
852
853 my $mpv = AnyEvent::MPV->new (
854 mpv => $MPV,
855 args => \@MPV_ARGS,
856 on_event => sub {
857 input_feed "mpv/$_[1]", $_[2];
858 },
859 on_key => sub {
860 input_feed $_[1];
861 },
862 on_eof => sub {
863 input_feed "mpv/quit";
864 },
865 );
866
867 ...
868
869 $mpv->start ("--idle=yes", "--pause", "--force-window=no");
870
871It also doesn't use complicated command line arguments - the file search
872options have the most impact, as they prevent F<mpv> from scanning
873directories with tens of thousands of files for subtitles and more:
874
875 --audio-client-name=doomfrontend
876 --osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95
877 --sub-auto=exact --audio-file-auto=exact
878
879Since it runs on a TV without a desktop environemnt, it tries to keep complications such as dbus
880away and the screensaver happy:
881
882 # prevent xscreensaver from doing something stupid, such as starting dbus
883 $ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure
884 $ENV{XDG_CURRENT_DESKTOP} = "generic";
885
886It does bind a number of keys to internal (to doomfrontend) commands:
887
888 for (
889 List::Util::pairs qw(
890 ESC return
891 q return
892 ENTER enter
893 SPACE pause
894 [ steprev
895 ] stepfwd
896 j subtitle
897 BS red
898 i green
899 o yellow
900 b blue
901 D triangle
902 UP up
903 DOWN down
904 RIGHT right
905 LEFT left
906 ),
907 (map { ("KP$_" => "num$_") } 0..9),
908 KP_INS => 0, # KP0, but different
909 ) {
910 $mpv->bind_key ($_->[0] => $_->[1]);
911 }
912
913It also reacts to sponsorblock chapters, so it needs to know when vidoe
914chapters change. Preadting C<AnyEvent::MPV>, it handles observers
915manually:
916
917 $mpv->cmd (observe_property => 1, "chapter-metadata");
918
919It also tries to apply an F<mpv> profile, if it exists:
920
921 eval {
922 # the profile is optional
923 $mpv->cmd ("apply-profile" => "doomfrontend");
924 };
925
926Most of the complicated parts deal with saving and restoring per-video
927data, such as bookmarks, playing position, selected audio and subtitle
928tracks and so on. However, since it uses L<Coro>, it can conveniently
929block and wait for replies, which is n ot possible in purely event based
930programs, as you are not allowed to block inside event callbacks in most
931event loops. This simplifies the code quite a bit.
932
933When the file to be played is a Tv recording done by mythtv, it uses the
934C<appending> protocol and deinterlacing:
935
936 if (is_myth $mpv_path) {
937 $mpv_path = "appending://$mpv_path";
938 $initial_deinterlace = 1;
939 }
940
941Otherwise, it sets some defaults and loads the file (I forgot what the
942C<dummy> argument is for, but I am sure it is needed by some F<mpv>
943version):
944
945 $mpv->cmd ("script-message", "osc-visibility", "never", "dummy");
946 $mpv->cmd ("set", "vid", "auto");
947 $mpv->cmd ("set", "aid", "auto");
948 $mpv->cmd ("set", "sid", "no");
949 $mpv->cmd ("set", "file-local-options/chapters-file", $mpv->escape_binary ("$mpv_path.chapters"));
950 $mpv->cmd ("loadfile", $mpv->escape_binary ($mpv_path));
951 $mpv->cmd ("script-message", "osc-visibility", "auto", "dummy");
952
953Handling events makes the main bulk of video playback code. For example,
954various ways of ending playback:
955
956 if ($INPUT eq "mpv/quit") { # should not happen, but allows user to kill etc. without consequence
957 $status = 1;
958 mpv_init; # try reinit
959 last;
960
961 } elsif ($INPUT eq "mpv/idle") { # normal end-of-file
962 last;
963
964 } elsif ($INPUT eq "return") {
965 $status = 1;
966 last;
967
968Or the code that actually starts playback, once the file is loaded:
969
970 our %SAVE_PROPERTY = (aid => 1, sid => 1, "audio-delay" => 1);
971
972 ...
973
974 my $oid = 100;
975
976 } elsif ($INPUT eq "mpv/file-loaded") { # start playing, configure video
977 $mpv->cmd ("seek", $playback_start, "absolute+exact") if $playback_start > 0;
978
979 my $target_fps = eval { $mpv->cmd_recv ("get_property", "container-fps") } || 60;
980 $target_fps *= play_video_speed_mult;
981 set_fps $target_fps;
982
983 unless (eval { $mpv->cmd_recv ("get_property", "video-format") }) {
984 $mpv->cmd ("set", "file-local-options/lavfi-complex", "[aid1] asplit [ao], showcqt=..., format=yuv420p [vo]");
985 };
986
987 for my $prop (keys %SAVE_PROPERTY) {
988 if (exists $PLAYING_STATE->{"mpv_$prop"}) {
989 $mpv->cmd ("set", "$prop", $PLAYING_STATE->{"mpv_$prop"} . "");
990 }
991
992 $mpv->cmd ("observe_property", ++$oid, $prop);
993 }
994
995 play_video_set_speed;
996 $mpv->cmd ("set", "osd-level", "$OSD_LEVEL");
997 $mpv->cmd ("observe_property", ++$oid, "osd-level");
998 $mpv->cmd ("set", "pause", "no");
999
1000 $mpv->cmd ("set_property", "deinterlace", "yes")
1001 if $initial_deinterlace;
1002
1003There is a lot going on here. First it seeks to the actual playback
1004position, if it is not at the start of the file (it would probaby be more
1005efficient to set the starting position before loading the file, though,
1006but this is good enough).
1007
1008Then it plays with the display fps, to set it to something harmonious
1009w.r.t. the video framerate.
1010
1011If the file does not have a video part, it assumes it is an audio file and
1012sets a visualizer.
1013
1014Also, a number of properties are not global, but per-file. At the moment,
1015this is C<audio-delay>, and the current audio/subtitle track, which it
1016sets, and also creates an observer. Again, this doesn'T use the observe
1017functionality of this module, but handles it itself, assigning obsevrer
1018ids 100+ to temporary/per-file observers.
1019
1020Lastly, it sets some global (or per-youtube-uploader) parameters, such as
1021speed, and unpauses. Property changes are handled like other input events:
1022
1023 } elsif ($INPUT eq "mpv/property-change") {
1024 my $prop = $INPUT_DATA->{name};
1025
1026 if ($prop eq "chapter-metadata") {
1027 if ($INPUT_DATA->{data}{TITLE} =~ /^\[SponsorBlock\]: (.*)/) {
1028 my $section = $1;
1029 my $skip;
1030
1031 $skip ||= $SPONSOR_SKIP{$_}
1032 for split /\s*,\s*/, $section;
1033
1034 if (defined $skip) {
1035 if ($skip) {
1036 # delay a bit, in case we get two metadata changes in quick succession, e.g.
1037 # because we have a skip at file load time.
1038 $skip_delay = AE::timer 2/50, 0, sub {
1039 $mpv->cmd ("no-osd", "add", "chapter", 1);
1040 $mpv->cmd ("show-text", "skipped sponsorblock section \"$section\"", 3000);
1041 };
1042 } else {
1043 undef $skip_delay;
1044 $mpv->cmd ("show-text", "NOT skipping sponsorblock section \"$section\"", 3000);
1045 }
1046 } else {
1047 $mpv->cmd ("show-text", "UNRECOGNIZED sponsorblock section \"$section\"", 60000);
1048 }
1049 } else {
1050 # cancel a queued skip
1051 undef $skip_delay;
1052 }
1053
1054 } elsif (exists $SAVE_PROPERTY{$prop}) {
1055 $PLAYING_STATE->{"mpv_$prop"} = $INPUT_DATA->{data};
1056 ::state_save;
1057 }
1058
1059This saves back the per-file properties, and also handles chapter changes
1060in a hacky way.
1061
1062Most of the handlers are very simple, though. For example:
1063
1064 } elsif ($INPUT eq "pause") {
1065 $mpv->cmd ("cycle", "pause");
1066 $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
1067 } elsif ($INPUT eq "right") {
1068 $mpv->cmd ("osd-msg-bar", "seek", 30, "relative+exact");
1069 } elsif ($INPUT eq "left") {
1070 $mpv->cmd ("osd-msg-bar", "seek", -5, "relative+exact");
1071 } elsif ($INPUT eq "up") {
1072 $mpv->cmd ("osd-msg-bar", "seek", +600, "relative+exact");
1073 } elsif ($INPUT eq "down") {
1074 $mpv->cmd ("osd-msg-bar", "seek", -600, "relative+exact");
1075 } elsif ($INPUT eq "select") {
1076 $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "-0.100");
1077 } elsif ($INPUT eq "start") {
1078 $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "0.100");
1079 } elsif ($INPUT eq "intfwd") {
1080 $mpv->cmd ("no-osd", "frame-step");
1081 } elsif ($INPUT eq "audio") {
1082 $mpv->cmd ("osd-auto", "cycle", "audio");
1083 } elsif ($INPUT eq "subtitle") {
1084 $mpv->cmd ("osd-auto", "cycle", "sub");
1085 } elsif ($INPUT eq "triangle") {
1086 $mpv->cmd ("osd-auto", "cycle", "deinterlace");
1087
1088Once a file has finished playing (or the user strops playback), it pauses,
1089unobserves the per-file observers, and saves the current position for to
1090be able to resume:
1091
1092 $mpv->cmd ("set", "pause", "yes");
1093
1094 while ($oid > 100) {
1095 $mpv->cmd ("unobserve_property", $oid--);
1096 }
1097
1098 $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time");
1099
1100And thats most of the F<mpv>-related code.
1101
1102=head2 F<Gtk2::CV>
1103
1104F<Gtk2::CV> is low-feature image viewer that I use many times daily
1105because it can handle directories with millions of files without falling
1106over. It also had the ability to play videos for ages, but it used an
1107older, crappier protocol to talk to F<mpv> and used F<ffprobe> before
1108playing each file instead of letting F<mpv> handle format/size detection.
1109
1110After writing this module, I decided to upgprade Gtk2::CV by making use
1111of it, with the goal of getting rid of F<ffprobe> and being ablew to
1112reuse F<mpv> processes, which would have a multitude of speed benefits
1113(for example, fork+exec of F<mpv> caused the kernel to close all file
1114descriptors, which could take minutes if a large file was being copied via
1115NFS, as the kernel waited for thr buffers to be flushed on close - not
1116having to start F<mpv> gets rid of this issue).
1117
1118Setting up is only complicated by the fact that F<mpv> needs to be
1119embedded into an existing window. To keep control of all inputs,
1120F<Gtk2::CV> puts an eventbox in front of F<mpv>, so F<mpv> receives no
1121input events:
1122
1123 $self->{mpv} = AnyEvent::MPV->new (
1124 trace => $ENV{CV_MPV_TRACE},
1125 );
1126
1127 # create an eventbox, so we receive all input events
1128 my $box = $self->{mpv_eventbox} = new Gtk2::EventBox;
1129 $box->set_above_child (1);
1130 $box->set_visible_window (0);
1131 $box->set_events ([]);
1132 $box->can_focus (0);
1133
1134 # create a drawingarea that mpv can display into
1135 my $window = $self->{mpv_window} = new Gtk2::DrawingArea;
1136 $box->add ($window);
1137
1138 # put the drawingarea intot he eventbox, and the eventbox into our display window
1139 $self->add ($box);
1140
1141 # we need to pass the window id to F<mpv>, which means we need to realise
1142 # the drawingarea, so an X window is allocated for it.
1143 $self->show_all;
1144 $window->realize;
1145 my $xid = $window->window->get_xid;
1146
1147Then it starts F<mpv> using this setup:
1148
1149 local $ENV{LC_ALL} = "POSIX";
1150 $self->{mpv}->start (
1151 "--no-terminal",
1152 "--no-input-terminal",
1153 "--no-input-default-bindings",
1154 "--no-input-cursor",
1155 "--input-conf=/dev/null",
1156 "--input-vo-keyboard=no",
1157
1158 "--loop-file=inf",
1159 "--force-window=yes",
1160 "--idle=yes",
1161
1162 "--audio-client-name=CV",
1163
1164 "--osc=yes", # --osc=no displays fading play/pause buttons instead
1165
1166 "--wid=$xid",
1167 );
1168
1169 $self->{mpv}->cmd ("script-message" => "osc-visibility" => "never", "dummy");
1170 $self->{mpv}->cmd ("osc-idlescreen" => "no");
1171
1172It also prepares a hack to force a ConfigureNotify event on every vidoe
1173reconfig:
1174
1175 # force a configurenotify on every video-reconfig
1176 $self->{mpv_reconfig} = $self->{mpv}->register_event (video_reconfig => sub {
1177 my ($mpv, $event, $data) = @_;
1178
1179 $self->mpv_window_update;
1180 });
1181
1182The way this is done is by doing a "dummy" resize to 1x1 and back:
1183
1184 $self->{mpv_window}->window->resize (1, 1),
1185 $self->{mpv_window}->window->resize ($self->{w}, $self->{h});
1186
1187Without this, F<mpv> often doesn't "get" the correct window size. Doing
1188it this way is not nice, but I didn't fine a nicer way to do it.
1189
1190When no file is being played, F<mpv> is hidden and prepared:
1191
1192 $self->{mpv_eventbox}->hide;
1193
1194 $self->{mpv}->cmd (set_property => "pause" => "yes");
1195 $self->{mpv}->cmd ("playlist_remove", "current");
1196 $self->{mpv}->cmd (set_property => "video-rotate" => 0);
1197 $self->{mpv}->cmd (set_property => "lavfi-complex" => "");
1198
1199Loading a file is a bit more complicated, as bluray and DVD rips are
1200supported:
1201
1202 if ($moviedir) {
1203 if ($moviedir eq "br") {
1204 $mpv->cmd (set => "bluray-device" => $path);
1205 $mpv->cmd (loadfile => "bd://");
1206 } elsif ($moviedir eq "dvd") {
1207 $mpv->cmd (set => "dvd-device" => $path);
1208 $mpv->cmd (loadfile => "dvd://");
1209 }
1210 } elsif ($type eq "video/iso-bluray") {
1211 $mpv->cmd (set => "bluray-device" => $path);
1212 $mpv->cmd (loadfile => "bd://");
1213 } else {
1214 $mpv->cmd (loadfile => $mpv->escape_binary ($path));
1215 }
1216
1217After this, C<Gtk2::CV> waits for the file to be loaded, video to be
1218configured, and then queries the video size (to resize its own window)
1219and video format (to decide whether an audio visualizer is needed for
1220audio playback). The problematic word here is "wait", as this needs to be
1221imploemented using callbacks.
1222
1223This made the code much harder to write, as the whole setup is very
1224asynchronous (C<Gtk2::CV> talks to the command interface in F<mpv>, which
1225talks to the decode and playback parts, all of which run asynchronously
1226w.r.t. each other. In practise, this can mean that C<Gtk2::CV> waits for
1227a file to be loaded by F<mpv> while the command interface of F<mpv> still
1228deals with the previous file and the decoder still handles an even older
1229file). Adding to this fact is that Gtk2::CV is bound by the glib event
1230loop, which means we cannot wait for replies form F<mpv> anywhere, so
1231everything has to be chained callbacks.
1232
1233The way this is handled is by creating a new empty hash ref that is unique
1234for each loaded file, and use it to detect whether the event is old or
1235not, and also store C<AnyEvent::MPV> guard objects in it:
1236
1237 # every time we loaded a file, we create a new hash
1238 my $guards = $self->{mpv_guards} = { };
1239
1240Then, when we wait for an event to occur, delete the handler, and, if the
1241C<mpv_guards> object has changed, we ignore it. Something like this:
1242
1243 $guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
1244 delete $guards->{file_loaded};
1245 return if $guards != $self->{mpv_guards};
1246
1247Commands do not have guards since they cnanot be cancelled, so we don't
1248have to do this for commands. But what prevents us form misinterpreting
1249an old event? Since F<mpv> (by default) handles commands synchronously,
1250we can queue a dummy command, whose only purpose is to tell us when all
1251previous commands are done. We use C<get_version> for this.
1252
1253The simplified code looks like this:
1254
1255 Scalar::Util::weaken $self;
1256
1257 $mpv->cmd ("get_version")->cb (sub {
1258
1259 $guards->{file_loaded} = $mpv->register_event (file_loaded => sub {
1260 delete $guards->{file_loaded};
1261 return if $guards != $self->{mpv_guards};
1262
1263 $mpv->cmd (get_property => "video-format")->cb (sub {
1264 return if $guards != $self->{mpv_guards};
1265
1266 # video-format handling
1267 return if eval { $_[0]->recv; 1 };
1268
1269 # no video? assume audio and visualize, cpu usage be damned
1270 $mpv->cmd (set => "lavfi-complex" => ...");
1271 });
1272
1273 $guards->{show} = $mpv->register_event (video_reconfig => sub {
1274 delete $guards->{show};
1275 return if $guards != $self->{mpv_guards};
1276
1277 $self->{mpv_eventbox}->show_all;
1278
1279 $w = $mpv->cmd (get_property => "dwidth");
1280 $h = $mpv->cmd (get_property => "dheight");
1281
1282 $h->cb (sub {
1283 $w = eval { $w->recv };
1284 $h = eval { $h->recv };
1285
1286 $mpv->cmd (set_property => "pause" => "no");
1287
1288 if ($w && $h) {
1289 # resize our window
1290 }
1291
1292 });
1293 });
1294
1295 });
1296
1297 });
1298
1299Most of the rest of the code is much simpler and just deals with forwarding user commands:
1300
1301 } elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10");
1302 } elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10");
1303 } elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60");
1304 } elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60");
1305 } elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio");
1306 } elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub");
1307 } elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2");
1308 } elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause");
1309 } elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2");
1310 } elsif ($key == $Gtk2::Gdk::Keysyms{0} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "+2");
1311
827=head1 SEE ALSO 1312=head1 SEE ALSO
828 1313
829L<AnyEvent>, L<the mpv command documentation|https://mpv.io/manual/stable/#command-interface>. 1314L<AnyEvent>, L<the mpv command documentation|https://mpv.io/manual/stable/#command-interface>.
830 1315
831=head1 AUTHOR 1316=head1 AUTHOR

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines