--- AnyEvent-MPV/README 2023/03/19 19:36:40 1.1 +++ AnyEvent-MPV/README 2023/03/22 19:37:00 1.4 @@ -4,12 +4,953 @@ SYNOPSIS use AnyEvent::MPV; + my $videofile = "path/to/file.mkv"; + use AnyEvent; + my $mpv = AnyEvent::MPV->new (trace => 1); + $mpv->start ("--idle=yes"); + $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); + my $quit = AE::cv; + $mpv->register_event (end_file => $quit); + $quit->recv; + DESCRIPTION - This module is an AnyEvent user, you need to make sure that you use and - run a supported event loop. + This module allows you to remote control mpv (a video player). It also + is an AnyEvent user, you need to make sure that you use and run a + supported event loop. + + There are other modules doing this, and I haven't looked much at them + other than to decide that they don't handle encodings correctly, and + since none of them use AnyEvent, I wrote my own. When in doubt, have a + look at them, too. + + Knowledge of the mpv command interface + is required to use + this module. + + Features of this module are: + + uses AnyEvent, so integrates well into most event-based programs + supports asynchronous and synchronous operation + allows you to properly pass binary filenames + accepts data encoded in any way (does not crash when mpv replies with + non UTF-8 data) + features a simple keybind/event system + + OVERVIEW OF OPERATION + This module forks an mpv process and uses --input-ipc-client (or + equivalent) to create a bidirectional communication channel between it + and the mpv process. + + It then speaks the somewhat JSON-looking (but not really being JSON) + protocol that mpv implements to both send it commands, decode and handle + replies, and handle asynchronous events. + + Here is a very simple client: + + use AnyEvent; + use AnyEvent::MPV; + + my $videofile = "./xyzzy.mkv"; + + my $mpv = AnyEvent::MPV->new (trace => 1); + + $mpv->start ("--", $videofile); + + my $timer = AE::timer 2, 0, my $quit = AE::cv; + $quit->recv; + + This starts mpv with the two arguments "--" and $videofile, which it + should load and play. It then waits two seconds by starting a timer and + quits. The "trace" argument to the constructor makes mpv more verbose + and also prints the commands and responses, so you can have an idea what + is going on. + + In my case, the above example would output something like this: + + [uosc] Disabled because original osc is enabled! + mpv> {"event":"start-file","playlist_entry_id":1} + mpv> {"event":"tracks-changed"} + (+) Video --vid=1 (*) (h264 480x480 30.000fps) + mpv> {"event":"metadata-update"} + mpv> {"event":"file-loaded"} + Using hardware decoding (nvdec). + mpv> {"event":"video-reconfig"} + VO: [gpu] 480x480 cuda[nv12] + mpv> {"event":"video-reconfig"} + mpv> {"event":"playback-restart"} + + This is not usually very useful (you could just run mpv as a simple + shell command), so let us load the file at runtime: + + use AnyEvent; + use AnyEvent::MPV; + + my $videofile = "./xyzzy.mkv"; + + my $mpv = AnyEvent::MPV->new ( + trace => 1, + args => ["--pause", "--idle=yes"], + ); + + $mpv->start; + $mpv->cmd_recv (loadfile => $mpv->escape_binary ($videofile)); + $mpv->cmd ("set", "pause", "no"); + + my $timer = AE::timer 2, 0, my $quit = AE::cv; + $quit->recv; + + This specifies extra arguments in the constructor - these arguments are + used every time you "->start" mpv, while the arguments to "->start" are + only used for this specific clal to0 "start". The argument --pause keeps + mpv in pause mode (i.e. it does not play the file after loading it), and + "--idle=yes" tells mpv to not quit when it does not have a playlist - as + no files are specified on the command line. + + To load a file, we then send it a "loadfile" command, which accepts, as + first argument, the URL or path to a video file. To make sure mpv does + not misinterpret the path as a URL, it was prefixed with ./ (similarly + to "protecting" paths in perls "open"). + + Since commands send *to* mpv are send in UTF-8, we need to escape the + filename (which might be in any encoding) using the "esscape_binary" + method - this is not needed if your filenames are just ascii, or + magically get interpreted correctly, but if you accept arbitrary + filenamews (e.g. from the user), you need to do this. + + The "cmd_recv" method then queues the command, waits for a reply and + returns the reply data (or croaks on error). mpv would, at this point, + load the file and, if everything was successful, show the first frame + and pause. Note that, since mpv is implement rather synchronously + itself, do not expect commands to fail in many circumstances - for + example, fit he file does not exit, you will likely get an event, but + the "loadfile" command itself will run successfully. + + To unpause, we send another command, "set", to set the "pause" property + to "no", this time using the "cmd" method, which queues the command, but + instead of waiting for a reply, it immediately returns a condvar that + cna be used to receive results. + + This should then cause mpv to start playing the video. + + It then again waits two seconds and quits. + + Now, just waiting two seconds is rather, eh, unuseful, so let's look at + receiving events (using a somewhat embellished example): + + use AnyEvent; + use AnyEvent::MPV; + + my $videofile = "xyzzy.mkv"; + + my $quit = AE::cv; + + my $mpv = AnyEvent::MPV->new ( + trace => 1, + args => ["--pause", "--idle=yes"], + ); + + $mpv->start; + + $mpv->register_event (start_file => sub { + $mpv->cmd ("set", "pause", "no"); + }); + + $mpv->register_event (end_file => sub { + my ($mpv, $event, $data) = @_; + + print "end-file<$data->{reason}>\n"; + $quit->send; + }); + + $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); + + $quit->recv; + + This example uses a global condvar $quit to wait for the file to finish + playing. Also, most of the logic is now implement in event handlers. + + The two events handlers we register are "start-file", which is emitted + by mpv once it has loaded a new file, and "end-file", which signals the + end of a file (underscores are internally replaced by minus signs, so + you cna speicfy event names with either). + + In the "start-file" event, we again set the "pause" property to "no" so + the movie starts playing. For the "end-file" event, we tell the main + program to quit by invoking $quit. + + This should conclude the basics of operation. There are a few more + examples later in the documentation. + + ENCODING CONVENTIONS + As a rule of thumb, all data you pass to this module to be sent to mpv + is expected to be in unicode. To pass something that isn't, you need to + escape it using "escape_binary". + + Data received from mpv, however, is *not* decoded to unicode, as data + returned by mpv is not generally encoded in unicode, and the encoding is + usually unspecified. So if you receive data and expect it to be in + unicode, you need to first decode it from UTF-8, but note that this + might fail. This is not a limitation of this module - mpv simply does + not specify nor guarantee a specific encoding, or any encoding at all, + in its protocol. + + METHODS + $mpv = AnyEvent::MPV->new (key => value...) + Creates a new "mpv" object, but does not yet do anything. The + support key-value pairs are: + + mpv => $path + The path to the mpv binary to use - by default, "mpv" is used + and therefore, uses your "PATH" to find it. + + args => [...] + Arguments to pass to mpv. These arguments are passed after the + hardcoded arguments used by this module, but before the + arguments passed ot "start". It does not matter whether you + specify your arguments using this key, or in the "start" call, + but when you invoke mpv multiple times, typically the arguments + used for all invocations go here, while arguments used for + specific invocations (e..g filenames) are passed to "start". + + trace => false|true|coderef + Enables tracing if true. In trace mode, output from mpv is + printed to standard error using a "mpv>" prefix, and commands + sent to mpv are printed with a ">mpv" prefix. + + If a code reference is passed, then instead of printing to + standard errort, this coderef is invoked with a first arfgument + being either "mpv>" or ">mpv", and the second argument being a + string to display. The default implementation simply does this: + + sub { + warn "$_[0] $_[1]\n"; + } + + on_eof => $coderef->($mpv) + on_event => $coderef->($mpv, $event, $data) + on_key => $coderef->($mpv, $string) + These are invoked by the default method implementation of the + same name - see below. + + $string = $mpv->escape_binary ($string) + This module excects all command data sent to mpv to be in unicode. + Some things are not, such as filenames. To pass binary data such as + filenames through a comamnd, you need to escape it using this + method. + + The simplest example is a "loadfile" command: + + $mpv->cmd_recv (loadfile => $mpv->escape_binary ($path)); + + $started = $mpv->start (argument...) + Starts mpv, passing the given arguemnts as extra arguments to mpv. + If mpv is already running, it returns false, otherwise it returns a + true value, so you can easily start mpv on demand by calling "start" + just before using it, and if it is already running, it will not be + started again. + + The arguments passwd to mpv are a set of hardcoded built-in + arguments, followed by the arguments specified in the constructor, + followed by the arguments passwd to this method. The built-in + arguments currently are --no-input-terminal, --really-quiet (or + --quiet in "trace" mode), and "--input-ipc-client" (or equivalent). + + Some commonly used and/or even useful arguments you might want to + pass are: + + --idle=yes or --idle=once to keep mpv from quitting when you don't + specify a file to play. + --pause, to keep mpv from instantly starting to play a file, in case + you want to inspect/change properties first. + --force-window=no (or similar), to keep mpv from instantly opening a + window, or to force it to do so. + --audio-client-name=yourappname, to make sure audio streams are + associated witht eh right program. + --wid=id, to embed mpv into another application. + --no-terminal, --no-input-default-bindings, --no-input-cursor, + --input-conf=/dev/null, --input-vo-keyboard=no - to ensure only you + control input. + + The return value can be used to decide whether mpv needs + initializing: + + if ($mpv->start) { + $mpv->bind_key (...); + $mpv->cmd (set => property => value); + ... + } + + You can immediately starting sending commands when this method + returns, even if mpv has not yet started. + + $mpv->stop + Ensures that mpv is being stopped, by killing mpv with a "TERM" + signal if needed. After this, you can "->start" a new instance + again. + + $mpv->on_eof + This method is called when mpv quits - usually unexpectedly. The + default implementation will call the "on_eof" code reference + specified in the constructor, or do nothing if none was given. + + For subclassing, see *SUBCLASSING*, below. + + $mpv->on_event ($event, $data) + This method is called when mpv sends an asynchronous event. The + default implementation will call the "on_event" code reference + specified in the constructor, or do nothing if none was given. + + The first/implicit argument is the $mpv object, the second is the + event name (same as "$data->{event}", purely for convenience), and + the third argument is the event object as sent by mpv (sans "event" + key). See List of events + in its documentation. + + For subclassing, see *SUBCLASSING*, below. + + $mpv->on_key ($string) + Invoked when a key declared by "->bind_key" is pressed. The default + invokes the "on_key" code reference specified in the constructor + with the $mpv object and the key name as arguments, or do nothing if + none was given. + + For more details and examples, see the "bind_key" method. + + For subclassing, see *SUBCLASSING*, below. + + $mpv->cmd ($command => $arg, $arg...) + Queues a command to be sent to mpv, using the given arguments, and + immediately return a condvar. + + See the mpv documentation + for details + on individual commands. + + The condvar can be ignored: + + $mpv->cmd (set_property => "deinterlace", "yes"); + + Or it can be used to synchronously wait for the command results: + + $cv = $mpv->cmd (get_property => "video-format"); + $format = $cv->recv; + + # or simpler: + + $format = $mpv->cmd (get_property => "video-format")->recv; + + # or even simpler: + + $format = $mpv->cmd_recv (get_property => "video-format"); + + Or you can set a callback: + + $cv = $mpv->cmd (get_property => "video-format"); + $cv->cb (sub { + my $format = $_[0]->recv; + }); + + On error, the condvar will croak when "recv" is called. + + $result = $mpv->cmd_recv ($command => $arg, $arg...) + The same as calling "cmd" and immediately "recv" on its return + value. Useful when you don't want to mess with mpv asynchronously or + simply needs to have the result: + + $mpv->cmd_recv ("stop"); + $position = $mpv->cmd_recv ("get_property", "playback-time"); + + $mpv->bind_key ($INPUT => $string) + This is an extension implement by this module to make it easy to get + key events. The way this is implemented is to bind a + "client-message" witha first argument of "AnyEvent::MPV" and the + $string you passed. This $string is then passed to the "on_key" + handle when the key is proessed, e.g.: + + my $mpv = AnyEvent::MPV->new ( + on_key => sub { + my ($mpv, $key) = @_; + + if ($key eq "letmeout") { + print "user pressed escape\n"; + } + }, + ); + + $mpv_>bind_key (ESC => "letmeout"); + + You cna find a list of key names in the mpv documentation + . + + The key configuration is lost when mpv is stopped and must be + (re-)done after every "start". + + [$guard] = $mpv->register_event ($event => $coderef->($mpv, $event, + $data)) + This method registers a callback to be invoked for a specific event. + Whenever the event occurs, it calls the coderef with the $mpv + object, the $event name and the event object, just like the + "on_event" method. + + For a lst of events, see the mpv documentation + . Any underscore in + the event name is replaced by a minus sign, so you can specify event + names using underscores for easier quoting in Perl. + + In void context, the handler stays registered until "stop" is + called. In any other context, it returns a guard object that, when + destroyed, will unregister the handler. + + You can register multiple handlers for the same event, and this + method does not interfere with the "on_event" mechanism. That is, + you can completely ignore this method and handle events in a + "on_event" handler, or mix both approaches as you see fit. + + Note that unlike commands, event handlers are registered + immediately, that is, you can issue a command, then register an + event handler and then get an event for this handler *before* the + command is even sent to mpv. If this kind of race is an issue, you + can issue a dummy command such as "get_version" and register the + handler when the reply is received. + + [$guard] = $mpv->observe_property ($name => $coderef->($mpv, $name, + $value)) + [$guard] = $mpv->observe_property_string ($name => $coderef->($mpv, + $name, $value)) + These methods wrap a registry system around mpv's "observe_property" + and "observe_property_string" commands - every time the named + property changes, the coderef is invoked with the $mpv object, the + name of the property and the new value. + + For a list of properties that you can observe, see the mpv + documentation . + + Due to the (sane :) way mpv handles these requests, you will always + get a property cxhange event right after registering an observer + (meaning you don't have to query the current value), and it is also + possible to register multiple observers for the same property - they + will all be handled properly. + + When called in void context, the observer stays in place until mpv + is stopped. In any otrher context, these methods return a guard + object that, when it goes out of scope, unregisters the observe + using "unobserve_property". + + Internally, this method uses observer ids of 2**52 + (0x10000000000000) or higher - it will not interfere with lower + ovserver ids, so it is possible to completely ignore this system and + execute "observe_property" commands yourself, whilst listening to + "property-change" events - as long as your ids stay below 2**52. + + Example: register observers for changtes in "aid" and "sid". Note + that a dummy statement is added to make sure the method is called in + void context. + + sub register_observers { + my ($mpv) = @_; + + $mpv->observe_property (aid => sub { + my ($mpv, $name, $value) = @_; + print "property aid (=$name) has changed to $value\n"; + }); + + $mpv->observe_property (sid => sub { + my ($mpv, $name, $value) = @_; + print "property sid (=$name) has changed to $value\n"; + }); + + () # ensure the above method is called in void context + } + + SUBCLASSING + Like most perl objects, "AnyEvent::MPV" objects are implemented as + hashes, with the constructor simply storing all passed key-value pairs + in the object. If you want to subclass to provide your own "on_*" + methods, be my guest and rummage around in the internals as much as you + wish - the only guarantee that this module dcoes is that it will not use + keys with double colons in the name, so youc an use those, or chose to + simply not care and deal with the breakage. + + If you don't want to go to the effort of subclassing this module, you + can also specify all event handlers as constructor keys. + +EXAMPLES + Here are some real-world code snippets, thrown in here mainly to give + you some example code to copy. + + doomfrontend + At one point I replaced mythtv-frontend by my own terminal-based video + player (based on rxvt-unicode). I toyed with the diea of using mpv's + subtitle engine to create the user interface, but that is hard to use + since you don't know how big your letters are. It is also where most of + this modules code has originally been developed in. + + It uses a unified input queue to handle various remote controls, so its + event handling needs are very simple - it simply feeds all events into + the input queue: + + my $mpv = AnyEvent::MPV->new ( + mpv => $MPV, + args => \@MPV_ARGS, + on_event => sub { + input_feed "mpv/$_[1]", $_[2]; + }, + on_key => sub { + input_feed $_[1]; + }, + on_eof => sub { + input_feed "mpv/quit"; + }, + ); + + ... + + $mpv->start ("--idle=yes", "--pause", "--force-window=no"); + + It also doesn't use complicated command line arguments - the file search + options have the most impact, as they prevent mpv from scanning + directories with tens of thousands of files for subtitles and more: + + --audio-client-name=doomfrontend + --osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95 + --sub-auto=exact --audio-file-auto=exact + + Since it runs on a TV without a desktop environemnt, it tries to keep + complications such as dbus away and the screensaver happy: + + # prevent xscreensaver from doing something stupid, such as starting dbus + $ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure + $ENV{XDG_CURRENT_DESKTOP} = "generic"; + + It does bind a number of keys to internal (to doomfrontend) commands: + + for ( + List::Util::pairs qw( + ESC return + q return + ENTER enter + SPACE pause + [ steprev + ] stepfwd + j subtitle + BS red + i green + o yellow + b blue + D triangle + UP up + DOWN down + RIGHT right + LEFT left + ), + (map { ("KP$_" => "num$_") } 0..9), + KP_INS => 0, # KP0, but different + ) { + $mpv->bind_key ($_->[0] => $_->[1]); + } + + It also reacts to sponsorblock chapters, so it needs to know when vidoe + chapters change. Preadting "AnyEvent::MPV", it handles observers + manually: + + $mpv->cmd (observe_property => 1, "chapter-metadata"); + + It also tries to apply an mpv profile, if it exists: + + eval { + # the profile is optional + $mpv->cmd ("apply-profile" => "doomfrontend"); + }; + + Most of the complicated parts deal with saving and restoring per-video + data, such as bookmarks, playing position, selected audio and subtitle + tracks and so on. However, since it uses Coro, it can conveniently block + and wait for replies, which is n ot possible in purely event based + programs, as you are not allowed to block inside event callbacks in most + event loops. This simplifies the code quite a bit. + + When the file to be played is a Tv recording done by mythtv, it uses the + "appending" protocol and deinterlacing: + + if (is_myth $mpv_path) { + $mpv_path = "appending://$mpv_path"; + $initial_deinterlace = 1; + } + + Otherwise, it sets some defaults and loads the file (I forgot what the + "dummy" argument is for, but I am sure it is needed by some mpv + version): + + $mpv->cmd ("script-message", "osc-visibility", "never", "dummy"); + $mpv->cmd ("set", "vid", "auto"); + $mpv->cmd ("set", "aid", "auto"); + $mpv->cmd ("set", "sid", "no"); + $mpv->cmd ("set", "file-local-options/chapters-file", $mpv->escape_binary ("$mpv_path.chapters")); + $mpv->cmd ("loadfile", $mpv->escape_binary ($mpv_path)); + $mpv->cmd ("script-message", "osc-visibility", "auto", "dummy"); + + Handling events makes the main bulk of video playback code. For example, + various ways of ending playback: + + if ($INPUT eq "mpv/quit") { # should not happen, but allows user to kill etc. without consequence + $status = 1; + mpv_init; # try reinit + last; + + } elsif ($INPUT eq "mpv/idle") { # normal end-of-file + last; + + } elsif ($INPUT eq "return") { + $status = 1; + last; + + Or the code that actually starts playback, once the file is loaded: + + our %SAVE_PROPERTY = (aid => 1, sid => 1, "audio-delay" => 1); + + ... + + my $oid = 100; + + } elsif ($INPUT eq "mpv/file-loaded") { # start playing, configure video + $mpv->cmd ("seek", $playback_start, "absolute+exact") if $playback_start > 0; + + my $target_fps = eval { $mpv->cmd_recv ("get_property", "container-fps") } || 60; + $target_fps *= play_video_speed_mult; + set_fps $target_fps; + + unless (eval { $mpv->cmd_recv ("get_property", "video-format") }) { + $mpv->cmd ("set", "file-local-options/lavfi-complex", "[aid1] asplit [ao], showcqt=..., format=yuv420p [vo]"); + }; + + for my $prop (keys %SAVE_PROPERTY) { + if (exists $PLAYING_STATE->{"mpv_$prop"}) { + $mpv->cmd ("set", "$prop", $PLAYING_STATE->{"mpv_$prop"} . ""); + } + + $mpv->cmd ("observe_property", ++$oid, $prop); + } + + play_video_set_speed; + $mpv->cmd ("set", "osd-level", "$OSD_LEVEL"); + $mpv->cmd ("observe_property", ++$oid, "osd-level"); + $mpv->cmd ("set", "pause", "no"); + + $mpv->cmd ("set_property", "deinterlace", "yes") + if $initial_deinterlace; + + There is a lot going on here. First it seeks to the actual playback + position, if it is not at the start of the file (it would probaby be + more efficient to set the starting position before loading the file, + though, but this is good enough). + + Then it plays with the display fps, to set it to something harmonious + w.r.t. the video framerate. + + If the file does not have a video part, it assumes it is an audio file + and sets a visualizer. + + Also, a number of properties are not global, but per-file. At the + moment, this is "audio-delay", and the current audio/subtitle track, + which it sets, and also creates an observer. Again, this doesn'T use the + observe functionality of this module, but handles it itself, assigning + obsevrer ids 100+ to temporary/per-file observers. + + Lastly, it sets some global (or per-youtube-uploader) parameters, such + as speed, and unpauses. Property changes are handled like other input + events: + + } elsif ($INPUT eq "mpv/property-change") { + my $prop = $INPUT_DATA->{name}; + + if ($prop eq "chapter-metadata") { + if ($INPUT_DATA->{data}{TITLE} =~ /^\[SponsorBlock\]: (.*)/) { + my $section = $1; + my $skip; + + $skip ||= $SPONSOR_SKIP{$_} + for split /\s*,\s*/, $section; + + if (defined $skip) { + if ($skip) { + # delay a bit, in case we get two metadata changes in quick succession, e.g. + # because we have a skip at file load time. + $skip_delay = AE::timer 2/50, 0, sub { + $mpv->cmd ("no-osd", "add", "chapter", 1); + $mpv->cmd ("show-text", "skipped sponsorblock section \"$section\"", 3000); + }; + } else { + undef $skip_delay; + $mpv->cmd ("show-text", "NOT skipping sponsorblock section \"$section\"", 3000); + } + } else { + $mpv->cmd ("show-text", "UNRECOGNIZED sponsorblock section \"$section\"", 60000); + } + } else { + # cancel a queued skip + undef $skip_delay; + } + + } elsif (exists $SAVE_PROPERTY{$prop}) { + $PLAYING_STATE->{"mpv_$prop"} = $INPUT_DATA->{data}; + ::state_save; + } + + This saves back the per-file properties, and also handles chapter + changes in a hacky way. + + Most of the handlers are very simple, though. For example: + + } elsif ($INPUT eq "pause") { + $mpv->cmd ("cycle", "pause"); + $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time"); + } elsif ($INPUT eq "right") { + $mpv->cmd ("osd-msg-bar", "seek", 30, "relative+exact"); + } elsif ($INPUT eq "left") { + $mpv->cmd ("osd-msg-bar", "seek", -5, "relative+exact"); + } elsif ($INPUT eq "up") { + $mpv->cmd ("osd-msg-bar", "seek", +600, "relative+exact"); + } elsif ($INPUT eq "down") { + $mpv->cmd ("osd-msg-bar", "seek", -600, "relative+exact"); + } elsif ($INPUT eq "select") { + $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "-0.100"); + } elsif ($INPUT eq "start") { + $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "0.100"); + } elsif ($INPUT eq "intfwd") { + $mpv->cmd ("no-osd", "frame-step"); + } elsif ($INPUT eq "audio") { + $mpv->cmd ("osd-auto", "cycle", "audio"); + } elsif ($INPUT eq "subtitle") { + $mpv->cmd ("osd-auto", "cycle", "sub"); + } elsif ($INPUT eq "triangle") { + $mpv->cmd ("osd-auto", "cycle", "deinterlace"); + + Once a file has finished playing (or the user strops playback), it + pauses, unobserves the per-file observers, and saves the current + position for to be able to resume: + + $mpv->cmd ("set", "pause", "yes"); + + while ($oid > 100) { + $mpv->cmd ("unobserve_property", $oid--); + } + + $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time"); + + And thats most of the mpv-related code. + + Gtk2::CV + Gtk2::CV is low-feature image viewer that I use many times daily because + it can handle directories with millions of files without falling over. + It also had the ability to play videos for ages, but it used an older, + crappier protocol to talk to mpv and used ffprobe before playing each + file instead of letting mpv handle format/size detection. + + After writing this module, I decided to upgprade Gtk2::CV by making use + of it, with the goal of getting rid of ffprobe and being ablew to reuse + mpv processes, which would have a multitude of speed benefits (for + example, fork+exec of mpv caused the kernel to close all file + descriptors, which could take minutes if a large file was being copied + via NFS, as the kernel waited for thr buffers to be flushed on close - + not having to start mpv gets rid of this issue). + + Setting up is only complicated by the fact that mpv needs to be embedded + into an existing window. To keep control of all inputs, Gtk2::CV puts an + eventbox in front of mpv, so mpv receives no input events: + + $self->{mpv} = AnyEvent::MPV->new ( + trace => $ENV{CV_MPV_TRACE}, + ); + + # create an eventbox, so we receive all input events + my $box = $self->{mpv_eventbox} = new Gtk2::EventBox; + $box->set_above_child (1); + $box->set_visible_window (0); + $box->set_events ([]); + $box->can_focus (0); + + # create a drawingarea that mpv can display into + my $window = $self->{mpv_window} = new Gtk2::DrawingArea; + $box->add ($window); + + # put the drawingarea intot he eventbox, and the eventbox into our display window + $self->add ($box); + + # we need to pass the window id to F, which means we need to realise + # the drawingarea, so an X window is allocated for it. + $self->show_all; + $window->realize; + my $xid = $window->window->get_xid; + + Then it starts mpv using this setup: + + local $ENV{LC_ALL} = "POSIX"; + $self->{mpv}->start ( + "--no-terminal", + "--no-input-terminal", + "--no-input-default-bindings", + "--no-input-cursor", + "--input-conf=/dev/null", + "--input-vo-keyboard=no", + + "--loop-file=inf", + "--force-window=yes", + "--idle=yes", + + "--audio-client-name=CV", + + "--osc=yes", # --osc=no displays fading play/pause buttons instead + + "--wid=$xid", + ); + + $self->{mpv}->cmd ("script-message" => "osc-visibility" => "never", "dummy"); + $self->{mpv}->cmd ("osc-idlescreen" => "no"); + + It also prepares a hack to force a ConfigureNotify event on every vidoe + reconfig: + + # force a configurenotify on every video-reconfig + $self->{mpv_reconfig} = $self->{mpv}->register_event (video_reconfig => sub { + my ($mpv, $event, $data) = @_; + + $self->mpv_window_update; + }); + + The way this is done is by doing a "dummy" resize to 1x1 and back: + + $self->{mpv_window}->window->resize (1, 1), + $self->{mpv_window}->window->resize ($self->{w}, $self->{h}); + + Without this, mpv often doesn't "get" the correct window size. Doing it + this way is not nice, but I didn't fine a nicer way to do it. + + When no file is being played, mpv is hidden and prepared: + + $self->{mpv_eventbox}->hide; + + $self->{mpv}->cmd (set_property => "pause" => "yes"); + $self->{mpv}->cmd ("playlist_remove", "current"); + $self->{mpv}->cmd (set_property => "video-rotate" => 0); + $self->{mpv}->cmd (set_property => "lavfi-complex" => ""); + + Loading a file is a bit more complicated, as bluray and DVD rips are + supported: + + if ($moviedir) { + if ($moviedir eq "br") { + $mpv->cmd (set => "bluray-device" => $path); + $mpv->cmd (loadfile => "bd://"); + } elsif ($moviedir eq "dvd") { + $mpv->cmd (set => "dvd-device" => $path); + $mpv->cmd (loadfile => "dvd://"); + } + } elsif ($type eq "video/iso-bluray") { + $mpv->cmd (set => "bluray-device" => $path); + $mpv->cmd (loadfile => "bd://"); + } else { + $mpv->cmd (loadfile => $mpv->escape_binary ($path)); + } + + After this, "Gtk2::CV" waits for the file to be loaded, video to be + configured, and then queries the video size (to resize its own window) + and video format (to decide whether an audio visualizer is needed for + audio playback). The problematic word here is "wait", as this needs to + be imploemented using callbacks. + + This made the code much harder to write, as the whole setup is very + asynchronous ("Gtk2::CV" talks to the command interface in mpv, which + talks to the decode and playback parts, all of which run asynchronously + w.r.t. each other. In practise, this can mean that "Gtk2::CV" waits for + a file to be loaded by mpv while the command interface of mpv still + deals with the previous file and the decoder still handles an even older + file). Adding to this fact is that Gtk2::CV is bound by the glib event + loop, which means we cannot wait for replies form mpv anywhere, so + everything has to be chained callbacks. + + The way this is handled is by creating a new empty hash ref that is + unique for each loaded file, and use it to detect whether the event is + old or not, and also store "AnyEvent::MPV" guard objects in it: + + # every time we loaded a file, we create a new hash + my $guards = $self->{mpv_guards} = { }; + + Then, when we wait for an event to occur, delete the handler, and, if + the "mpv_guards" object has changed, we ignore it. Something like this: + + $guards->{file_loaded} = $mpv->register_event (file_loaded => sub { + delete $guards->{file_loaded}; + return if $guards != $self->{mpv_guards}; + + Commands do not have guards since they cnanot be cancelled, so we don't + have to do this for commands. But what prevents us form misinterpreting + an old event? Since mpv (by default) handles commands synchronously, we + can queue a dummy command, whose only purpose is to tell us when all + previous commands are done. We use "get_version" for this. + + The simplified code looks like this: + + Scalar::Util::weaken $self; + + $mpv->cmd ("get_version")->cb (sub { + + $guards->{file_loaded} = $mpv->register_event (file_loaded => sub { + delete $guards->{file_loaded}; + return if $guards != $self->{mpv_guards}; + + $mpv->cmd (get_property => "video-format")->cb (sub { + return if $guards != $self->{mpv_guards}; + + # video-format handling + return if eval { $_[0]->recv; 1 }; + + # no video? assume audio and visualize, cpu usage be damned + $mpv->cmd (set => "lavfi-complex" => ..."); + }); + + $guards->{show} = $mpv->register_event (video_reconfig => sub { + delete $guards->{show}; + return if $guards != $self->{mpv_guards}; + + $self->{mpv_eventbox}->show_all; + + $w = $mpv->cmd (get_property => "dwidth"); + $h = $mpv->cmd (get_property => "dheight"); + + $h->cb (sub { + $w = eval { $w->recv }; + $h = eval { $h->recv }; + + $mpv->cmd (set_property => "pause" => "no"); + + if ($w && $h) { + # resize our window + } + + }); + }); + + }); + + }); + + Most of the rest of the code is much simpler and just deals with + forwarding user commands: + + } elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10"); + } elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10"); + } elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60"); + } elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60"); + } elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio"); + } elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub"); + } elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2"); + } elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause"); + } elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2"); + } elsif ($key == $Gtk2::Gdk::Keysyms{0} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "+2"); SEE ALSO - AnyEvent. + AnyEvent, the mpv command documentation + . AUTHOR Marc Lehmann