1 | NAME |
1 | NAME |
2 | AnyEvent::MPV - remote control mpv (https://mpv.io) |
2 | AnyEvent::MPV - remote control mpv (https://mpv.io) |
3 | |
3 | |
4 | SYNOPSIS |
4 | SYNOPSIS |
5 | use AnyEvent::MPV; |
5 | use AnyEvent::MPV; |
|
|
6 | |
|
|
7 | my $videofile = "path/to/file.mkv"; |
|
|
8 | use AnyEvent; |
|
|
9 | my $mpv = AnyEvent::MPV->new (trace => 1); |
|
|
10 | $mpv->start ("--idle=yes"); |
|
|
11 | $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); |
|
|
12 | my $quit = AE::cv; |
|
|
13 | $mpv->register_event (end_file => $quit); |
|
|
14 | $quit->recv; |
6 | |
15 | |
7 | DESCRIPTION |
16 | DESCRIPTION |
8 | This module allows you to remote control mpv (a video player). It also |
17 | This module allows you to remote control mpv (a video player). It also |
9 | is an AnyEvent user, you need to make sure that you use and run a |
18 | is an AnyEvent user, you need to make sure that you use and run a |
10 | supported event loop. |
19 | supported event loop. |
… | |
… | |
39 | Here is a very simple client: |
48 | Here is a very simple client: |
40 | |
49 | |
41 | use AnyEvent; |
50 | use AnyEvent; |
42 | use AnyEvent::MPV; |
51 | use AnyEvent::MPV; |
43 | |
52 | |
44 | my $videofile = "./xyzzy.mp4"; |
53 | my $videofile = "./xyzzy.mkv"; |
45 | |
54 | |
46 | my $mpv = AnyEvent::MPV->new (trace => 1); |
55 | my $mpv = AnyEvent::MPV->new (trace => 1); |
47 | |
56 | |
48 | $mpv->start ("--", $videofile); |
57 | $mpv->start ("--", $videofile); |
49 | |
58 | |
… | |
… | |
74 | shell command), so let us load the file at runtime: |
83 | shell command), so let us load the file at runtime: |
75 | |
84 | |
76 | use AnyEvent; |
85 | use AnyEvent; |
77 | use AnyEvent::MPV; |
86 | use AnyEvent::MPV; |
78 | |
87 | |
79 | my $videofile = "./xyzzy.mp4"; |
88 | my $videofile = "./xyzzy.mkv"; |
80 | |
89 | |
81 | my $mpv = AnyEvent::MPV->new ( |
90 | my $mpv = AnyEvent::MPV->new ( |
82 | trace => 1, |
91 | trace => 1, |
83 | args => ["--pause", "--idle=yes"], |
92 | args => ["--pause", "--idle=yes"], |
84 | ); |
93 | ); |
… | |
… | |
90 | my $timer = AE::timer 2, 0, my $quit = AE::cv; |
99 | my $timer = AE::timer 2, 0, my $quit = AE::cv; |
91 | $quit->recv; |
100 | $quit->recv; |
92 | |
101 | |
93 | This specifies extra arguments in the constructor - these arguments are |
102 | This specifies extra arguments in the constructor - these arguments are |
94 | used every time you "->start" mpv, while the arguments to "->start" are |
103 | used every time you "->start" mpv, while the arguments to "->start" are |
95 | only used for this specific clal to0 "start". The argument --pause keeps |
104 | only used for this specific call to "start". The argument --pause keeps |
96 | mpv in pause mode (i.e. it does not play the file after loading it), and |
105 | mpv in pause mode (i.e. it does not play the file after loading it), and |
97 | "--idle=yes" tells mpv to not quit when it does not have a playlist - as |
106 | "--idle=yes" tells mpv to not quit when it does not have a playlist - as |
98 | no files are specified on the command line. |
107 | no files are specified on the command line. |
99 | |
108 | |
100 | To load a file, we then send it a "loadfile" command, which accepts, as |
109 | To load a file, we then send it a "loadfile" command, which accepts, as |
101 | first argument, the URL or path to a video file. To make sure mpv does |
110 | first argument, the URL or path to a video file. To make sure mpv does |
102 | not misinterpret the path as a URL, it was prefixed with ./ (similarly |
111 | not misinterpret the path as a URL, it was prefixed with ./ (similarly |
103 | to "protecting" paths in perls "open"). |
112 | to "protecting" paths in perls "open"). |
104 | |
113 | |
105 | Since commands send *to* mpv are send in UTF-8, we need to escape the |
114 | Since commands send *to* mpv are send in UTF-8, we need to escape the |
106 | filename (which might be in any encoding) using the "esscape_binary" |
115 | filename (which might be in any encoding) using the "escape_binary" |
107 | method - this is not needed if your filenames are just ascii, or |
116 | method - this is not needed if your filenames are just ASCII, or |
108 | magically get interpreted correctly, but if you accept arbitrary |
117 | magically get interpreted correctly, but if you accept arbitrary |
109 | filenamews (e.g. from the user), you need to do this. |
118 | filenames (e.g. from the user), you need to do this. |
110 | |
119 | |
111 | The "cmd_recv" method then queues the command, waits for a reply and |
120 | The "cmd_recv" method then queues the command, waits for a reply and |
112 | returns the reply data (or croaks on error). mpv would, at this point, |
121 | returns the reply data (or croaks on error). mpv would, at this point, |
113 | load the file and, if everything was successful, show the first frame |
122 | load the file and, if everything was successful, show the first frame |
114 | and pause. Note that, since mpv is implement rather synchronously |
123 | and pause. Note that, since mpv is implement rather synchronously |
… | |
… | |
117 | the "loadfile" command itself will run successfully. |
126 | the "loadfile" command itself will run successfully. |
118 | |
127 | |
119 | To unpause, we send another command, "set", to set the "pause" property |
128 | To unpause, we send another command, "set", to set the "pause" property |
120 | to "no", this time using the "cmd" method, which queues the command, but |
129 | to "no", this time using the "cmd" method, which queues the command, but |
121 | instead of waiting for a reply, it immediately returns a condvar that |
130 | instead of waiting for a reply, it immediately returns a condvar that |
122 | cna be used to receive results. |
131 | can be used to receive results. |
123 | |
132 | |
124 | This should then cause mpv to start playing the video. |
133 | This should then cause mpv to start playing the video. |
125 | |
134 | |
126 | It then again waits two seconds and quits. |
135 | It then again waits two seconds and quits. |
127 | |
136 | |
… | |
… | |
129 | receiving events (using a somewhat embellished example): |
138 | receiving events (using a somewhat embellished example): |
130 | |
139 | |
131 | use AnyEvent; |
140 | use AnyEvent; |
132 | use AnyEvent::MPV; |
141 | use AnyEvent::MPV; |
133 | |
142 | |
134 | my $videofile = "xyzzy.mp4"; |
143 | my $videofile = "xyzzy.mkv"; |
135 | |
144 | |
136 | my $quit = AE::cv; |
145 | my $quit = AE::cv; |
137 | |
146 | |
138 | my $mpv = AnyEvent::MPV->new ( |
147 | my $mpv = AnyEvent::MPV->new ( |
139 | trace => 1, |
148 | trace => 1, |
140 | args => ["--pause", "--idle=yes"], |
149 | args => ["--pause", "--idle=yes"], |
141 | on_event => sub { |
|
|
142 | my ($mpv, $event, $data) = @_; |
|
|
143 | |
|
|
144 | if ($event eq "start-file") { |
|
|
145 | $mpv->cmd ("set", "pause", "no"); |
|
|
146 | } elsif ($event eq "end-file") { |
|
|
147 | print "end-file<$data->{reason}>\n"; |
|
|
148 | $quit->send; |
|
|
149 | } |
|
|
150 | }, |
|
|
151 | ); |
150 | ); |
152 | |
151 | |
153 | $mpv->start; |
152 | $mpv->start; |
|
|
153 | |
|
|
154 | $mpv->register_event (start_file => sub { |
|
|
155 | $mpv->cmd ("set", "pause", "no"); |
|
|
156 | }); |
|
|
157 | |
|
|
158 | $mpv->register_event (end_file => sub { |
|
|
159 | my ($mpv, $event, $data) = @_; |
|
|
160 | |
|
|
161 | print "end-file<$data->{reason}>\n"; |
|
|
162 | $quit->send; |
|
|
163 | }); |
|
|
164 | |
154 | $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); |
165 | $mpv->cmd (loadfile => $mpv->escape_binary ($videofile)); |
155 | |
166 | |
156 | $quit->recv; |
167 | $quit->recv; |
157 | |
168 | |
158 | This example uses a global condvar $quit to wait for the file to finish |
169 | This example uses a global condvar $quit to wait for the file to finish |
159 | playing. Also, most of the logic is now in an "on_event" callback, which |
170 | playing. Also, most of the logic is now implement in event handlers. |
160 | receives an event name and the actual event object. |
|
|
161 | |
171 | |
162 | The two events we handle are "start-file", which is emitted by mpv once |
172 | The two events handlers we register are "start-file", which is emitted |
163 | it has loaded a new file, and "end-file", which signals the end of a |
173 | by mpv once it has loaded a new file, and "end-file", which signals the |
164 | file. |
174 | end of a file (underscores are internally replaced by minus signs, so |
|
|
175 | you can specify event names with either). |
165 | |
176 | |
166 | In the former event, we again set the "pause" property to "no" so the |
177 | In the "start-file" event, we again set the "pause" property to "no" so |
167 | movie starts playing. For the latter event, we tell the main program to |
178 | the movie starts playing. For the "end-file" event, we tell the main |
168 | quit by invoking $quit. |
179 | program to quit by invoking $quit. |
169 | |
180 | |
170 | This should conclude the basics of operation. There are a few more |
181 | This should conclude the basics of operation. There are a few more |
171 | examples later in the documentation. |
182 | examples later in the documentation. |
172 | |
183 | |
173 | ENCODING CONVENTIONS |
184 | ENCODING CONVENTIONS |
174 | As a rule of thumb, all data you pass to this module to be sent to mpv |
185 | As a rule of thumb, all data you pass to this module to be sent to mpv |
175 | is expected to be in unicode. To pass something that isn't, you need to |
186 | is expected to be in unicode. To pass something that isn't, you need to |
176 | escape it using "escape_binary". |
187 | escape it using "escape_binary". |
177 | |
188 | |
178 | Data received from $mpv, however, is *not* decoded to unicode, as data |
189 | Data received from mpv, however, is *not* decoded to unicode, as data |
179 | returned by mpv is not generally encoded in unicode, and the encoding is |
190 | returned by mpv is not generally encoded in unicode, and the encoding is |
180 | usually unspecified. So if you receive data and expect it to be in |
191 | usually unspecified. So if you receive data and expect it to be in |
181 | unicode, you need to first decode it from UTF-8, but note that this |
192 | unicode, you need to first decode it from UTF-8, but note that this |
182 | might fail. This is not a limitation of this module - mpv simply does |
193 | might fail. This is not a limitation of this module - mpv simply does |
183 | not specify nor guarantee a specific encoding, or any encoding at all, |
194 | not specify nor guarantee a specific encoding, or any encoding at all, |
… | |
… | |
205 | Enables tracing if true. In trace mode, output from mpv is |
216 | Enables tracing if true. In trace mode, output from mpv is |
206 | printed to standard error using a "mpv>" prefix, and commands |
217 | printed to standard error using a "mpv>" prefix, and commands |
207 | sent to mpv are printed with a ">mpv" prefix. |
218 | sent to mpv are printed with a ">mpv" prefix. |
208 | |
219 | |
209 | If a code reference is passed, then instead of printing to |
220 | If a code reference is passed, then instead of printing to |
210 | standard errort, this coderef is invoked with a first arfgument |
221 | standard error, this coderef is invoked with a first argument |
211 | being either "mpv>" or ">mpv", and the second argument being a |
222 | being either "mpv>" or ">mpv", and the second argument being a |
212 | string to display. The default implementation simply does this: |
223 | string to display. The default implementation simply does this: |
213 | |
224 | |
214 | sub { |
225 | sub { |
215 | warn "$_[0] $_[1]\n"; |
226 | warn "$_[0] $_[1]\n"; |
… | |
… | |
220 | on_key => $coderef->($mpv, $string) |
231 | on_key => $coderef->($mpv, $string) |
221 | These are invoked by the default method implementation of the |
232 | These are invoked by the default method implementation of the |
222 | same name - see below. |
233 | same name - see below. |
223 | |
234 | |
224 | $string = $mpv->escape_binary ($string) |
235 | $string = $mpv->escape_binary ($string) |
225 | This module excects all command data sent to mpv to be in unicode. |
236 | This module expects all command data sent to mpv to be in unicode. |
226 | Some things are not, such as filenames. To pass binary data such as |
237 | Some things are not, such as filenames. To pass binary data such as |
227 | filenames through a comamnd, you need to escape it using this |
238 | filenames through a command, you need to escape it using this |
228 | method. |
239 | method. |
229 | |
240 | |
230 | The simplest example is a "loadfile" command: |
241 | The simplest example is a "loadfile" command: |
231 | |
242 | |
232 | $mpv->cmd_recv (loadfile => $mpv->escape_binary ($path)); |
243 | $mpv->cmd_recv (loadfile => $mpv->escape_binary ($path)); |
233 | |
244 | |
234 | $started = $mpv->start (argument...) |
245 | $started = $mpv->start (argument...) |
235 | Starts mpv, passing the given arguemnts as extra arguments to mpv. |
246 | Starts mpv, passing the given arguments as extra arguments to mpv. |
236 | If mpv is already running, it returns false, otherwise it returns a |
247 | If mpv is already running, it returns false, otherwise it returns a |
237 | true value, so you can easily start mpv on demand by calling "start" |
248 | true value, so you can easily start mpv on demand by calling "start" |
238 | just before using it, and if it is already running, it will not be |
249 | just before using it, and if it is already running, it will not be |
239 | started again. |
250 | started again. |
240 | |
251 | |
241 | The arguments passwd to mpv are a set of hardcoded built-in |
252 | The arguments passed to mpv are a set of hard-coded built-in |
242 | arguments, followed by the arguments specified in the constructor, |
253 | arguments, followed by the arguments specified in the constructor, |
243 | followed by the arguments passwd to this method. The built-in |
254 | followed by the arguments passed to this method. The built-in |
244 | arguments currently are --no-input-terminal, --really-quiet (or |
255 | arguments currently are --no-input-terminal, --really-quiet (or |
245 | --quiet in "trace" mode), and "--input-ipc-client" (or equivalent). |
256 | --quiet in "trace" mode), and "--input-ipc-client" (or equivalent). |
246 | |
257 | |
247 | Some commonly used and/or even useful arguments you might want to |
258 | Some commonly used and/or even useful arguments you might want to |
248 | pass are: |
259 | pass are: |
… | |
… | |
289 | default implementation will call the "on_event" code reference |
300 | default implementation will call the "on_event" code reference |
290 | specified in the constructor, or do nothing if none was given. |
301 | specified in the constructor, or do nothing if none was given. |
291 | |
302 | |
292 | The first/implicit argument is the $mpv object, the second is the |
303 | The first/implicit argument is the $mpv object, the second is the |
293 | event name (same as "$data->{event}", purely for convenience), and |
304 | event name (same as "$data->{event}", purely for convenience), and |
294 | the third argument is the full event object as sent by mpv. See List |
305 | the third argument is the event object as sent by mpv (sans "event" |
|
|
306 | key). See List of events |
295 | of events <https://mpv.io/manual/stable/#list-of-events> in its |
307 | <https://mpv.io/manual/stable/#list-of-events> in its documentation. |
296 | documentation. |
|
|
297 | |
308 | |
298 | For subclassing, see *SUBCLASSING*, below. |
309 | For subclassing, see *SUBCLASSING*, below. |
299 | |
310 | |
300 | $mpv->on_key ($string) |
311 | $mpv->on_key ($string) |
301 | Invoked when a key declared by "->bind_key" is pressed. The default |
312 | Invoked when a key declared by "->bind_key" is pressed. The default |
… | |
… | |
350 | $position = $mpv->cmd_recv ("get_property", "playback-time"); |
361 | $position = $mpv->cmd_recv ("get_property", "playback-time"); |
351 | |
362 | |
352 | $mpv->bind_key ($INPUT => $string) |
363 | $mpv->bind_key ($INPUT => $string) |
353 | This is an extension implement by this module to make it easy to get |
364 | This is an extension implement by this module to make it easy to get |
354 | key events. The way this is implemented is to bind a |
365 | key events. The way this is implemented is to bind a |
355 | "client-message" witha first argument of "AnyEvent::MPV" and the |
366 | "client-message" with a first argument of "AnyEvent::MPV" and the |
356 | $string you passed. This $string is then passed ot the "on_key" |
367 | $string you passed. This $string is then passed to the "on_key" |
357 | handle when the key is proessed, e.g.: |
368 | handle when the key is processed, e.g.: |
358 | |
369 | |
359 | my $mpv = AnyEvent::MPV->new ( |
370 | my $mpv = AnyEvent::MPV->new ( |
360 | on_key => sub { |
371 | on_key => sub { |
361 | my ($mpv, $key) = @_; |
372 | my ($mpv, $key) = @_; |
362 | |
373 | |
… | |
… | |
366 | }, |
377 | }, |
367 | ); |
378 | ); |
368 | |
379 | |
369 | $mpv_>bind_key (ESC => "letmeout"); |
380 | $mpv_>bind_key (ESC => "letmeout"); |
370 | |
381 | |
|
|
382 | You can find a list of key names in the mpv documentation |
|
|
383 | <https://mpv.io/manual/stable/#key-names>. |
|
|
384 | |
371 | The key configuration is lost when mpv is stopped and must be |
385 | The key configuration is lost when mpv is stopped and must be |
372 | (re-)done after every "start". |
386 | (re-)done after every "start". |
|
|
387 | |
|
|
388 | [$guard] = $mpv->register_event ($event => $coderef->($mpv, $event, |
|
|
389 | $data)) |
|
|
390 | This method registers a callback to be invoked for a specific event. |
|
|
391 | Whenever the event occurs, it calls the coderef with the $mpv |
|
|
392 | object, the $event name and the event object, just like the |
|
|
393 | "on_event" method. |
|
|
394 | |
|
|
395 | For a list of events, see the mpv documentation |
|
|
396 | <https://mpv.io/manual/stable/#list-of-events>. Any underscore in |
|
|
397 | the event name is replaced by a minus sign, so you can specify event |
|
|
398 | names using underscores for easier quoting in Perl. |
|
|
399 | |
|
|
400 | In void context, the handler stays registered until "stop" is |
|
|
401 | called. In any other context, it returns a guard object that, when |
|
|
402 | destroyed, will unregister the handler. |
|
|
403 | |
|
|
404 | You can register multiple handlers for the same event, and this |
|
|
405 | method does not interfere with the "on_event" mechanism. That is, |
|
|
406 | you can completely ignore this method and handle events in a |
|
|
407 | "on_event" handler, or mix both approaches as you see fit. |
|
|
408 | |
|
|
409 | Note that unlike commands, event handlers are registered |
|
|
410 | immediately, that is, you can issue a command, then register an |
|
|
411 | event handler and then get an event for this handler *before* the |
|
|
412 | command is even sent to mpv. If this kind of race is an issue, you |
|
|
413 | can issue a dummy command such as "get_version" and register the |
|
|
414 | handler when the reply is received. |
|
|
415 | |
|
|
416 | [$guard] = $mpv->observe_property ($name => $coderef->($mpv, $name, |
|
|
417 | $value)) |
|
|
418 | [$guard] = $mpv->observe_property_string ($name => $coderef->($mpv, |
|
|
419 | $name, $value)) |
|
|
420 | These methods wrap a registry system around mpv's "observe_property" |
|
|
421 | and "observe_property_string" commands - every time the named |
|
|
422 | property changes, the coderef is invoked with the $mpv object, the |
|
|
423 | name of the property and the new value. |
|
|
424 | |
|
|
425 | For a list of properties that you can observe, see the mpv |
|
|
426 | documentation <https://mpv.io/manual/stable/#property-list>. |
|
|
427 | |
|
|
428 | Due to the (sane :) way mpv handles these requests, you will always |
|
|
429 | get a property change event right after registering an observer |
|
|
430 | (meaning you don't have to query the current value), and it is also |
|
|
431 | possible to register multiple observers for the same property - they |
|
|
432 | will all be handled properly. |
|
|
433 | |
|
|
434 | When called in void context, the observer stays in place until mpv |
|
|
435 | is stopped. In any other context, these methods return a guard |
|
|
436 | object that, when it goes out of scope, unregisters the observe |
|
|
437 | using "unobserve_property". |
|
|
438 | |
|
|
439 | Internally, this method uses observer ids of 2**52 |
|
|
440 | (0x10000000000000) or higher - it will not interfere with lower |
|
|
441 | observer ids, so it is possible to completely ignore this system and |
|
|
442 | execute "observe_property" commands yourself, whilst listening to |
|
|
443 | "property-change" events - as long as your ids stay below 2**52. |
|
|
444 | |
|
|
445 | Example: register observers for changes in "aid" and "sid". Note |
|
|
446 | that a dummy statement is added to make sure the method is called in |
|
|
447 | void context. |
|
|
448 | |
|
|
449 | sub register_observers { |
|
|
450 | my ($mpv) = @_; |
|
|
451 | |
|
|
452 | $mpv->observe_property (aid => sub { |
|
|
453 | my ($mpv, $name, $value) = @_; |
|
|
454 | print "property aid (=$name) has changed to $value\n"; |
|
|
455 | }); |
|
|
456 | |
|
|
457 | $mpv->observe_property (sid => sub { |
|
|
458 | my ($mpv, $name, $value) = @_; |
|
|
459 | print "property sid (=$name) has changed to $value\n"; |
|
|
460 | }); |
|
|
461 | |
|
|
462 | () # ensure the above method is called in void context |
|
|
463 | } |
373 | |
464 | |
374 | SUBCLASSING |
465 | SUBCLASSING |
375 | Like most perl objects, "AnyEvent::MPV" objects are implemented as |
466 | Like most perl objects, "AnyEvent::MPV" objects are implemented as |
376 | hashes, with the constructor simply storing all passed key-value pairs |
467 | hashes, with the constructor simply storing all passed key-value pairs |
377 | in the object. If you want to subclass to provide your own "on_*" |
468 | in the object. If you want to subclass to provide your own "on_*" |
378 | methods, be my guest and rummage around in the internals as much as you |
469 | methods, be my guest and rummage around in the internals as much as you |
379 | wish - the only guarantee that this module dcoes is that it will not use |
470 | wish - the only guarantee that this module does is that it will not use |
380 | keys with double colons in the name, so youc an use those, or chose to |
471 | keys with double colons in the name, so you can use those, or chose to |
381 | simply not care and deal with the breakage. |
472 | simply not care and deal with the breakage. |
382 | |
473 | |
383 | If you don't want to go to the effort of subclassing this module, you |
474 | If you don't want to go to the effort of subclassing this module, you |
384 | can also specify all event handlers as constructor keys. |
475 | can also specify all event handlers as constructor keys. |
|
|
476 | |
|
|
477 | EXAMPLES |
|
|
478 | Here are some real-world code snippets, thrown in here mainly to give |
|
|
479 | you some example code to copy. |
|
|
480 | |
|
|
481 | doomfrontend |
|
|
482 | At one point I replaced mythtv-frontend by my own terminal-based video |
|
|
483 | player (based on rxvt-unicode). I toyed with the idea of using mpv's |
|
|
484 | subtitle engine to create the user interface, but that is hard to use |
|
|
485 | since you don't know how big your letters are. It is also where most of |
|
|
486 | this modules code has originally been developed in. |
|
|
487 | |
|
|
488 | It uses a unified input queue to handle various remote controls, so its |
|
|
489 | event handling needs are very simple - it simply feeds all events into |
|
|
490 | the input queue: |
|
|
491 | |
|
|
492 | my $mpv = AnyEvent::MPV->new ( |
|
|
493 | mpv => $MPV, |
|
|
494 | args => \@MPV_ARGS, |
|
|
495 | on_event => sub { |
|
|
496 | input_feed "mpv/$_[1]", $_[2]; |
|
|
497 | }, |
|
|
498 | on_key => sub { |
|
|
499 | input_feed $_[1]; |
|
|
500 | }, |
|
|
501 | on_eof => sub { |
|
|
502 | input_feed "mpv/quit"; |
|
|
503 | }, |
|
|
504 | ); |
|
|
505 | |
|
|
506 | ... |
|
|
507 | |
|
|
508 | $mpv->start ("--idle=yes", "--pause", "--force-window=no"); |
|
|
509 | |
|
|
510 | It also doesn't use complicated command line arguments - the file search |
|
|
511 | options have the most impact, as they prevent mpv from scanning |
|
|
512 | directories with tens of thousands of files for subtitles and more: |
|
|
513 | |
|
|
514 | --audio-client-name=doomfrontend |
|
|
515 | --osd-on-seek=msg-bar --osd-bar-align-y=-0.85 --osd-bar-w=95 |
|
|
516 | --sub-auto=exact --audio-file-auto=exact |
|
|
517 | |
|
|
518 | Since it runs on a TV without a desktop environment, it tries to keep |
|
|
519 | complications such as dbus away and the screensaver happy: |
|
|
520 | |
|
|
521 | # prevent xscreensaver from doing something stupid, such as starting dbus |
|
|
522 | $ENV{DBUS_SESSION_BUS_ADDRESS} = "/"; # prevent dbus autostart for sure |
|
|
523 | $ENV{XDG_CURRENT_DESKTOP} = "generic"; |
|
|
524 | |
|
|
525 | It does bind a number of keys to internal (to doomfrontend) commands: |
|
|
526 | |
|
|
527 | for ( |
|
|
528 | List::Util::pairs qw( |
|
|
529 | ESC return |
|
|
530 | q return |
|
|
531 | ENTER enter |
|
|
532 | SPACE pause |
|
|
533 | [ steprev |
|
|
534 | ] stepfwd |
|
|
535 | j subtitle |
|
|
536 | BS red |
|
|
537 | i green |
|
|
538 | o yellow |
|
|
539 | b blue |
|
|
540 | D triangle |
|
|
541 | UP up |
|
|
542 | DOWN down |
|
|
543 | RIGHT right |
|
|
544 | LEFT left |
|
|
545 | ), |
|
|
546 | (map { ("KP$_" => "num$_") } 0..9), |
|
|
547 | KP_INS => 0, # KP0, but different |
|
|
548 | ) { |
|
|
549 | $mpv->bind_key ($_->[0] => $_->[1]); |
|
|
550 | } |
|
|
551 | |
|
|
552 | It also reacts to sponsorblock chapters, so it needs to know when video |
|
|
553 | chapters change. Predating "AnyEvent::MPV", it handles observers |
|
|
554 | manually instead of using "observe_property": |
|
|
555 | |
|
|
556 | $mpv->cmd (observe_property => 1, "chapter-metadata"); |
|
|
557 | |
|
|
558 | It also tries to apply an mpv profile, if it exists: |
|
|
559 | |
|
|
560 | eval { |
|
|
561 | # the profile is optional |
|
|
562 | $mpv->cmd ("apply-profile" => "doomfrontend"); |
|
|
563 | }; |
|
|
564 | |
|
|
565 | Most of the complicated parts deal with saving and restoring per-video |
|
|
566 | data, such as bookmarks, playing position, selected audio and subtitle |
|
|
567 | tracks and so on. However, since it uses Coro, it can conveniently block |
|
|
568 | and wait for replies, which is not possible in purely event based |
|
|
569 | programs, as you are not allowed to block inside event callbacks in most |
|
|
570 | event loops. This simplifies the code quite a bit. |
|
|
571 | |
|
|
572 | When the file to be played is a TV recording done by mythtv, it uses the |
|
|
573 | "appending" protocol and deinterlacing: |
|
|
574 | |
|
|
575 | if (is_myth $mpv_path) { |
|
|
576 | $mpv_path = "appending://$mpv_path"; |
|
|
577 | $initial_deinterlace = 1; |
|
|
578 | } |
|
|
579 | |
|
|
580 | Otherwise, it sets some defaults and loads the file (I forgot what the |
|
|
581 | "dummy" argument is for, but I am sure it is needed by some mpv |
|
|
582 | version): |
|
|
583 | |
|
|
584 | $mpv->cmd ("script-message", "osc-visibility", "never", "dummy"); |
|
|
585 | $mpv->cmd ("set", "vid", "auto"); |
|
|
586 | $mpv->cmd ("set", "aid", "auto"); |
|
|
587 | $mpv->cmd ("set", "sid", "no"); |
|
|
588 | $mpv->cmd ("set", "file-local-options/chapters-file", $mpv->escape_binary ("$mpv_path.chapters")); |
|
|
589 | $mpv->cmd ("loadfile", $mpv->escape_binary ($mpv_path)); |
|
|
590 | $mpv->cmd ("script-message", "osc-visibility", "auto", "dummy"); |
|
|
591 | |
|
|
592 | Handling events makes the main bulk of video playback code. For example, |
|
|
593 | various ways of ending playback: |
|
|
594 | |
|
|
595 | if ($INPUT eq "mpv/quit") { # should not happen, but allows user to kill etc. without consequence |
|
|
596 | $status = 1; |
|
|
597 | mpv_init; # try reinit |
|
|
598 | last; |
|
|
599 | |
|
|
600 | } elsif ($INPUT eq "mpv/idle") { # normal end-of-file |
|
|
601 | last; |
|
|
602 | |
|
|
603 | } elsif ($INPUT eq "return") { |
|
|
604 | $status = 1; |
|
|
605 | last; |
|
|
606 | |
|
|
607 | Or the code that actually starts playback, once the file is loaded: |
|
|
608 | |
|
|
609 | our %SAVE_PROPERTY = (aid => 1, sid => 1, "audio-delay" => 1); |
|
|
610 | |
|
|
611 | ... |
|
|
612 | |
|
|
613 | my $oid = 100; |
|
|
614 | |
|
|
615 | } elsif ($INPUT eq "mpv/file-loaded") { # start playing, configure video |
|
|
616 | $mpv->cmd ("seek", $playback_start, "absolute+exact") if $playback_start > 0; |
|
|
617 | |
|
|
618 | my $target_fps = eval { $mpv->cmd_recv ("get_property", "container-fps") } || 60; |
|
|
619 | $target_fps *= play_video_speed_mult; |
|
|
620 | set_fps $target_fps; |
|
|
621 | |
|
|
622 | unless (eval { $mpv->cmd_recv ("get_property", "video-format") }) { |
|
|
623 | $mpv->cmd ("set", "file-local-options/lavfi-complex", "[aid1] asplit [ao], showcqt=..., format=yuv420p [vo]"); |
|
|
624 | }; |
|
|
625 | |
|
|
626 | for my $prop (keys %SAVE_PROPERTY) { |
|
|
627 | if (exists $PLAYING_STATE->{"mpv_$prop"}) { |
|
|
628 | $mpv->cmd ("set", "$prop", $PLAYING_STATE->{"mpv_$prop"} . ""); |
|
|
629 | } |
|
|
630 | |
|
|
631 | $mpv->cmd ("observe_property", ++$oid, $prop); |
|
|
632 | } |
|
|
633 | |
|
|
634 | play_video_set_speed; |
|
|
635 | $mpv->cmd ("set", "osd-level", "$OSD_LEVEL"); |
|
|
636 | $mpv->cmd ("observe_property", ++$oid, "osd-level"); |
|
|
637 | $mpv->cmd ("set", "pause", "no"); |
|
|
638 | |
|
|
639 | $mpv->cmd ("set_property", "deinterlace", "yes") |
|
|
640 | if $initial_deinterlace; |
|
|
641 | |
|
|
642 | There is a lot going on here. First it seeks to the actual playback |
|
|
643 | position, if it is not at the start of the file (it would probably be |
|
|
644 | more efficient to set the starting position before loading the file, |
|
|
645 | though, but this is good enough). |
|
|
646 | |
|
|
647 | Then it plays with the display fps, to set it to something harmonious |
|
|
648 | w.r.t. the video framerate. |
|
|
649 | |
|
|
650 | If the file does not have a video part, it assumes it is an audio file |
|
|
651 | and sets a visualizer. |
|
|
652 | |
|
|
653 | Also, a number of properties are not global, but per-file. At the |
|
|
654 | moment, this is "audio-delay", and the current audio/subtitle track, |
|
|
655 | which it sets, and also creates an observer. Again, this doesn't use the |
|
|
656 | observe functionality of this module, but handles it itself, assigning |
|
|
657 | observer ids 100+ to temporary/per-file observers. |
|
|
658 | |
|
|
659 | Lastly, it sets some global (or per-youtube-uploader) parameters, such |
|
|
660 | as speed, and unpauses. Property changes are handled like other input |
|
|
661 | events: |
|
|
662 | |
|
|
663 | } elsif ($INPUT eq "mpv/property-change") { |
|
|
664 | my $prop = $INPUT_DATA->{name}; |
|
|
665 | |
|
|
666 | if ($prop eq "chapter-metadata") { |
|
|
667 | if ($INPUT_DATA->{data}{TITLE} =~ /^\[SponsorBlock\]: (.*)/) { |
|
|
668 | my $section = $1; |
|
|
669 | my $skip; |
|
|
670 | |
|
|
671 | $skip ||= $SPONSOR_SKIP{$_} |
|
|
672 | for split /\s*,\s*/, $section; |
|
|
673 | |
|
|
674 | if (defined $skip) { |
|
|
675 | if ($skip) { |
|
|
676 | # delay a bit, in case we get two metadata changes in quick succession, e.g. |
|
|
677 | # because we have a skip at file load time. |
|
|
678 | $skip_delay = AE::timer 2/50, 0, sub { |
|
|
679 | $mpv->cmd ("no-osd", "add", "chapter", 1); |
|
|
680 | $mpv->cmd ("show-text", "skipped sponsorblock section \"$section\"", 3000); |
|
|
681 | }; |
|
|
682 | } else { |
|
|
683 | undef $skip_delay; |
|
|
684 | $mpv->cmd ("show-text", "NOT skipping sponsorblock section \"$section\"", 3000); |
|
|
685 | } |
|
|
686 | } else { |
|
|
687 | $mpv->cmd ("show-text", "UNRECOGNIZED sponsorblock section \"$section\"", 60000); |
|
|
688 | } |
|
|
689 | } else { |
|
|
690 | # cancel a queued skip |
|
|
691 | undef $skip_delay; |
|
|
692 | } |
|
|
693 | |
|
|
694 | } elsif (exists $SAVE_PROPERTY{$prop}) { |
|
|
695 | $PLAYING_STATE->{"mpv_$prop"} = $INPUT_DATA->{data}; |
|
|
696 | ::state_save; |
|
|
697 | } |
|
|
698 | |
|
|
699 | This saves back the per-file properties, and also handles chapter |
|
|
700 | changes in a hacky way. |
|
|
701 | |
|
|
702 | Most of the handlers are very simple, though. For example: |
|
|
703 | |
|
|
704 | } elsif ($INPUT eq "pause") { |
|
|
705 | $mpv->cmd ("cycle", "pause"); |
|
|
706 | $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time"); |
|
|
707 | } elsif ($INPUT eq "right") { |
|
|
708 | $mpv->cmd ("osd-msg-bar", "seek", 30, "relative+exact"); |
|
|
709 | } elsif ($INPUT eq "left") { |
|
|
710 | $mpv->cmd ("osd-msg-bar", "seek", -5, "relative+exact"); |
|
|
711 | } elsif ($INPUT eq "up") { |
|
|
712 | $mpv->cmd ("osd-msg-bar", "seek", +600, "relative+exact"); |
|
|
713 | } elsif ($INPUT eq "down") { |
|
|
714 | $mpv->cmd ("osd-msg-bar", "seek", -600, "relative+exact"); |
|
|
715 | } elsif ($INPUT eq "select") { |
|
|
716 | $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "-0.100"); |
|
|
717 | } elsif ($INPUT eq "start") { |
|
|
718 | $mpv->cmd ("osd-msg-bar", "add", "audio-delay", "0.100"); |
|
|
719 | } elsif ($INPUT eq "intfwd") { |
|
|
720 | $mpv->cmd ("no-osd", "frame-step"); |
|
|
721 | } elsif ($INPUT eq "audio") { |
|
|
722 | $mpv->cmd ("osd-auto", "cycle", "audio"); |
|
|
723 | } elsif ($INPUT eq "subtitle") { |
|
|
724 | $mpv->cmd ("osd-auto", "cycle", "sub"); |
|
|
725 | } elsif ($INPUT eq "triangle") { |
|
|
726 | $mpv->cmd ("osd-auto", "cycle", "deinterlace"); |
|
|
727 | |
|
|
728 | Once a file has finished playing (or the user strops playback), it |
|
|
729 | pauses, unobserves the per-file observers, and saves the current |
|
|
730 | position for to be able to resume: |
|
|
731 | |
|
|
732 | $mpv->cmd ("set", "pause", "yes"); |
|
|
733 | |
|
|
734 | while ($oid > 100) { |
|
|
735 | $mpv->cmd ("unobserve_property", $oid--); |
|
|
736 | } |
|
|
737 | |
|
|
738 | $PLAYING_STATE->{curpos} = $mpv->cmd_recv ("get_property", "playback-time"); |
|
|
739 | |
|
|
740 | And that's most of the mpv-related code. |
|
|
741 | |
|
|
742 | Gtk2::CV |
|
|
743 | Gtk2::CV is low-feature image viewer that I use many times daily because |
|
|
744 | it can handle directories with millions of files without falling over. |
|
|
745 | It also had the ability to play videos for ages, but it used an older, |
|
|
746 | crappier protocol to talk to mpv and used ffprobe before playing each |
|
|
747 | file instead of letting mpv handle format/size detection. |
|
|
748 | |
|
|
749 | After writing this module, I decided to upgrade Gtk2::CV by making use |
|
|
750 | of it, with the goal of getting rid of ffprobe and being able to reuse |
|
|
751 | mpv processes, which would have a multitude of speed benefits (for |
|
|
752 | example, fork+exec of mpv caused the kernel to close all file |
|
|
753 | descriptors, which could take minutes if a large file was being copied |
|
|
754 | via NFS, as the kernel waited for the buffers to be flushed on close - |
|
|
755 | not having to start mpv gets rid of this issue). |
|
|
756 | |
|
|
757 | Setting up is only complicated by the fact that mpv needs to be embedded |
|
|
758 | into an existing window. To keep control of all inputs, Gtk2::CV puts an |
|
|
759 | eventbox in front of mpv, so mpv receives no input events: |
|
|
760 | |
|
|
761 | $self->{mpv} = AnyEvent::MPV->new ( |
|
|
762 | trace => $ENV{CV_MPV_TRACE}, |
|
|
763 | ); |
|
|
764 | |
|
|
765 | # create an eventbox, so we receive all input events |
|
|
766 | my $box = $self->{mpv_eventbox} = new Gtk2::EventBox; |
|
|
767 | $box->set_above_child (1); |
|
|
768 | $box->set_visible_window (0); |
|
|
769 | $box->set_events ([]); |
|
|
770 | $box->can_focus (0); |
|
|
771 | |
|
|
772 | # create a drawingarea that mpv can display into |
|
|
773 | my $window = $self->{mpv_window} = new Gtk2::DrawingArea; |
|
|
774 | $box->add ($window); |
|
|
775 | |
|
|
776 | # put the drawingarea intot he eventbox, and the eventbox into our display window |
|
|
777 | $self->add ($box); |
|
|
778 | |
|
|
779 | # we need to pass the window id to F<mpv>, which means we need to realise |
|
|
780 | # the drawingarea, so an X window is allocated for it. |
|
|
781 | $self->show_all; |
|
|
782 | $window->realize; |
|
|
783 | my $xid = $window->window->get_xid; |
|
|
784 | |
|
|
785 | Then it starts mpv using this setup: |
|
|
786 | |
|
|
787 | local $ENV{LC_ALL} = "POSIX"; |
|
|
788 | $self->{mpv}->start ( |
|
|
789 | "--no-terminal", |
|
|
790 | "--no-input-terminal", |
|
|
791 | "--no-input-default-bindings", |
|
|
792 | "--no-input-cursor", |
|
|
793 | "--input-conf=/dev/null", |
|
|
794 | "--input-vo-keyboard=no", |
|
|
795 | |
|
|
796 | "--loop-file=inf", |
|
|
797 | "--force-window=yes", |
|
|
798 | "--idle=yes", |
|
|
799 | |
|
|
800 | "--audio-client-name=CV", |
|
|
801 | |
|
|
802 | "--osc=yes", # --osc=no displays fading play/pause buttons instead |
|
|
803 | |
|
|
804 | "--wid=$xid", |
|
|
805 | ); |
|
|
806 | |
|
|
807 | $self->{mpv}->cmd ("script-message" => "osc-visibility" => "never", "dummy"); |
|
|
808 | $self->{mpv}->cmd ("osc-idlescreen" => "no"); |
|
|
809 | |
|
|
810 | It also prepares a hack to force a ConfigureNotify event on every video |
|
|
811 | reconfig event: |
|
|
812 | |
|
|
813 | # force a configurenotify on every video-reconfig |
|
|
814 | $self->{mpv_reconfig} = $self->{mpv}->register_event (video_reconfig => sub { |
|
|
815 | my ($mpv, $event, $data) = @_; |
|
|
816 | |
|
|
817 | $self->mpv_window_update; |
|
|
818 | }); |
|
|
819 | |
|
|
820 | The way this is done is by doing a "dummy" resize to 1x1 and back: |
|
|
821 | |
|
|
822 | $self->{mpv_window}->window->resize (1, 1), |
|
|
823 | $self->{mpv_window}->window->resize ($self->{w}, $self->{h}); |
|
|
824 | |
|
|
825 | Without this, mpv often doesn't "get" the correct window size. Doing it |
|
|
826 | this way is not nice, but I didn't fine a nicer way to do it. |
|
|
827 | |
|
|
828 | When no file is being played, mpv is hidden and prepared: |
|
|
829 | |
|
|
830 | $self->{mpv_eventbox}->hide; |
|
|
831 | |
|
|
832 | $self->{mpv}->cmd (set_property => "pause" => "yes"); |
|
|
833 | $self->{mpv}->cmd ("playlist_remove", "current"); |
|
|
834 | $self->{mpv}->cmd (set_property => "video-rotate" => 0); |
|
|
835 | $self->{mpv}->cmd (set_property => "lavfi-complex" => ""); |
|
|
836 | |
|
|
837 | Loading a file is a bit more complicated, as blu-ray and DVD rips are |
|
|
838 | supported: |
|
|
839 | |
|
|
840 | if ($moviedir) { |
|
|
841 | if ($moviedir eq "br") { |
|
|
842 | $mpv->cmd (set => "bluray-device" => $path); |
|
|
843 | $mpv->cmd (loadfile => "bd://"); |
|
|
844 | } elsif ($moviedir eq "dvd") { |
|
|
845 | $mpv->cmd (set => "dvd-device" => $path); |
|
|
846 | $mpv->cmd (loadfile => "dvd://"); |
|
|
847 | } |
|
|
848 | } elsif ($type eq "video/iso-bluray") { |
|
|
849 | $mpv->cmd (set => "bluray-device" => $path); |
|
|
850 | $mpv->cmd (loadfile => "bd://"); |
|
|
851 | } else { |
|
|
852 | $mpv->cmd (loadfile => $mpv->escape_binary ($path)); |
|
|
853 | } |
|
|
854 | |
|
|
855 | After this, "Gtk2::CV" waits for the file to be loaded, video to be |
|
|
856 | configured, and then queries the video size (to resize its own window) |
|
|
857 | and video format (to decide whether an audio visualizer is needed for |
|
|
858 | audio playback). The problematic word here is "wait", as this needs to |
|
|
859 | be implemented using callbacks. |
|
|
860 | |
|
|
861 | This made the code much harder to write, as the whole setup is very |
|
|
862 | asynchronous ("Gtk2::CV" talks to the command interface in mpv, which |
|
|
863 | talks to the decode and playback parts, all of which run asynchronously |
|
|
864 | w.r.t. each other. In practise, this can mean that "Gtk2::CV" waits for |
|
|
865 | a file to be loaded by mpv while the command interface of mpv still |
|
|
866 | deals with the previous file and the decoder still handles an even older |
|
|
867 | file). Adding to this fact is that Gtk2::CV is bound by the glib event |
|
|
868 | loop, which means we cannot wait for replies form mpv anywhere, so |
|
|
869 | everything has to be chained callbacks. |
|
|
870 | |
|
|
871 | The way this is handled is by creating a new empty hash ref that is |
|
|
872 | unique for each loaded file, and use it to detect whether the event is |
|
|
873 | old or not, and also store "AnyEvent::MPV" guard objects in it: |
|
|
874 | |
|
|
875 | # every time we loaded a file, we create a new hash |
|
|
876 | my $guards = $self->{mpv_guards} = { }; |
|
|
877 | |
|
|
878 | Then, when we wait for an event to occur, delete the handler, and, if |
|
|
879 | the "mpv_guards" object has changed, we ignore it. Something like this: |
|
|
880 | |
|
|
881 | $guards->{file_loaded} = $mpv->register_event (file_loaded => sub { |
|
|
882 | delete $guards->{file_loaded}; |
|
|
883 | return if $guards != $self->{mpv_guards}; |
|
|
884 | |
|
|
885 | Commands do not have guards since they cannot be cancelled, so we don't |
|
|
886 | have to do this for commands. But what prevents us form misinterpreting |
|
|
887 | an old event? Since mpv (by default) handles commands synchronously, we |
|
|
888 | can queue a dummy command, whose only purpose is to tell us when all |
|
|
889 | previous commands are done. We use "get_version" for this. |
|
|
890 | |
|
|
891 | The simplified code looks like this: |
|
|
892 | |
|
|
893 | Scalar::Util::weaken $self; |
|
|
894 | |
|
|
895 | $mpv->cmd ("get_version")->cb (sub { |
|
|
896 | |
|
|
897 | $guards->{file_loaded} = $mpv->register_event (file_loaded => sub { |
|
|
898 | delete $guards->{file_loaded}; |
|
|
899 | return if $guards != $self->{mpv_guards}; |
|
|
900 | |
|
|
901 | $mpv->cmd (get_property => "video-format")->cb (sub { |
|
|
902 | return if $guards != $self->{mpv_guards}; |
|
|
903 | |
|
|
904 | # video-format handling |
|
|
905 | return if eval { $_[0]->recv; 1 }; |
|
|
906 | |
|
|
907 | # no video? assume audio and visualize, cpu usage be damned |
|
|
908 | $mpv->cmd (set => "lavfi-complex" => ..."); |
|
|
909 | }); |
|
|
910 | |
|
|
911 | $guards->{show} = $mpv->register_event (video_reconfig => sub { |
|
|
912 | delete $guards->{show}; |
|
|
913 | return if $guards != $self->{mpv_guards}; |
|
|
914 | |
|
|
915 | $self->{mpv_eventbox}->show_all; |
|
|
916 | |
|
|
917 | $w = $mpv->cmd (get_property => "dwidth"); |
|
|
918 | $h = $mpv->cmd (get_property => "dheight"); |
|
|
919 | |
|
|
920 | $h->cb (sub { |
|
|
921 | $w = eval { $w->recv }; |
|
|
922 | $h = eval { $h->recv }; |
|
|
923 | |
|
|
924 | $mpv->cmd (set_property => "pause" => "no"); |
|
|
925 | |
|
|
926 | if ($w && $h) { |
|
|
927 | # resize our window |
|
|
928 | } |
|
|
929 | |
|
|
930 | }); |
|
|
931 | }); |
|
|
932 | |
|
|
933 | }); |
|
|
934 | |
|
|
935 | }); |
|
|
936 | |
|
|
937 | Most of the rest of the code is much simpler and just deals with |
|
|
938 | forwarding user commands: |
|
|
939 | |
|
|
940 | } elsif ($key == $Gtk2::Gdk::Keysyms{Right}) { $mpv->cmd ("osd-msg-bar" => seek => "+10"); |
|
|
941 | } elsif ($key == $Gtk2::Gdk::Keysyms{Left} ) { $mpv->cmd ("osd-msg-bar" => seek => "-10"); |
|
|
942 | } elsif ($key == $Gtk2::Gdk::Keysyms{Up} ) { $mpv->cmd ("osd-msg-bar" => seek => "+60"); |
|
|
943 | } elsif ($key == $Gtk2::Gdk::Keysyms{Down} ) { $mpv->cmd ("osd-msg-bar" => seek => "-60"); |
|
|
944 | } elsif ($key == $Gtk2::Gdk::Keysyms{a}) ) { $mpv->cmd ("osd-msg-msg" => cycle => "audio"); |
|
|
945 | } elsif ($key == $Gtk2::Gdk::Keysyms{j} ) { $mpv->cmd ("osd-msg-msg" => cycle => "sub"); |
|
|
946 | } elsif ($key == $Gtk2::Gdk::Keysyms{o} ) { $mpv->cmd ("no-osd" => "cycle-values", "osd-level", "2", "3", "0", "2"); |
|
|
947 | } elsif ($key == $Gtk2::Gdk::Keysyms{p} ) { $mpv->cmd ("no-osd" => cycle => "pause"); |
|
|
948 | } elsif ($key == $Gtk2::Gdk::Keysyms{9} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "-2"); |
|
|
949 | } elsif ($key == $Gtk2::Gdk::Keysyms{0} ) { $mpv->cmd ("osd-msg-bar" => add => "ao-volume", "+2"); |
385 | |
950 | |
386 | SEE ALSO |
951 | SEE ALSO |
387 | AnyEvent, the mpv command documentation |
952 | AnyEvent, the mpv command documentation |
388 | <https://mpv.io/manual/stable/#command-interface>. |
953 | <https://mpv.io/manual/stable/#command-interface>. |
389 | |
954 | |