mpv-ipc 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +140 -0
- data/lib/mpv_ass/color.rb +111 -0
- data/lib/mpv_ass/span.rb +121 -0
- data/lib/mpv_ass/text.rb +91 -0
- data/lib/mpv_ass.rb +3 -0
- data/lib/mpv_ipc/client.rb +529 -0
- data/lib/mpv_ipc/exceptions.rb +55 -0
- data/lib/mpv_ipc/multi_queue.rb +76 -0
- data/lib/mpv_ipc/server.rb +122 -0
- data/lib/mpv_ipc/session.rb +170 -0
- data/lib/mpv_ipc/utils.rb +19 -0
- data/lib/mpv_ipc/version.rb +6 -0
- data/lib/mpv_ipc.rb +4 -0
- metadata +81 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "concurrent"
|
|
6
|
+
|
|
7
|
+
require_relative "exceptions"
|
|
8
|
+
require_relative "multi_queue"
|
|
9
|
+
require_relative "../mpv_ass"
|
|
10
|
+
|
|
11
|
+
module MPV
|
|
12
|
+
# Represents a connection to an `mpv` process over an IPC socket.
|
|
13
|
+
# @see https://mpv.io/manual/stable/#json-ipc
|
|
14
|
+
# MPV's IPC docs
|
|
15
|
+
# @see https://mpv.io/manual/stable/#properties
|
|
16
|
+
# MPV's property docs
|
|
17
|
+
class Client
|
|
18
|
+
# Alternative constructor for mpv embedded script mode.
|
|
19
|
+
# @return [MPV::Client] a new instance of this class
|
|
20
|
+
# using a file descriptor from `--mpv-ipc-fd` argument
|
|
21
|
+
def self.script
|
|
22
|
+
require "optparse"
|
|
23
|
+
OptionParser.new do |opts|
|
|
24
|
+
opts.on("--mpv-ipc-fd=N", Integer){ |fd| return new(fd) }
|
|
25
|
+
end.parse!
|
|
26
|
+
raise ArgumentError, "--mpv-ipc-fd argument not provided"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Creates a new MPV::Client instance.
|
|
30
|
+
# @param conn [Object] mpv connection descriptor
|
|
31
|
+
# It can be a Unix socket path as a [String], an already connected [Socket]
|
|
32
|
+
# or its raw file descriptor as an [Integer], or a custom IO-like object
|
|
33
|
+
# with standard gets() and puts() methods.
|
|
34
|
+
def initialize(conn)
|
|
35
|
+
@socket = case conn
|
|
36
|
+
when String then UNIXSocket.new(@path = conn)
|
|
37
|
+
when Integer then Socket.for_fd(conn)
|
|
38
|
+
else conn
|
|
39
|
+
end
|
|
40
|
+
@request_id = Concurrent::AtomicFixnum.new(MIN_REQ_ID - 1)
|
|
41
|
+
@reply_queue = MultiQueue.new(RESPONSE_TIMEOUT)
|
|
42
|
+
@event_listeners = Concurrent::Hash.new
|
|
43
|
+
@property_observers = Concurrent::Hash.new
|
|
44
|
+
@message_handlers = Concurrent::Hash.new
|
|
45
|
+
@keybinding_handlers = Concurrent::Hash.new
|
|
46
|
+
@osd_messages = Concurrent::Hash.new
|
|
47
|
+
@event_queue = Queue.new
|
|
48
|
+
@cmd_mutex = Mutex.new
|
|
49
|
+
@keybinding_mutex = Mutex.new
|
|
50
|
+
@osd_mutex = Mutex.new
|
|
51
|
+
@shutdown_callback = nil
|
|
52
|
+
@link_thread = Thread.new(&method(:link_loop))
|
|
53
|
+
@link_thread.name = "mpv link"
|
|
54
|
+
@event_thread = Thread.new(&method(:event_loop))
|
|
55
|
+
@event_thread.name = "mpv event"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Checks whether the mpv connection is still alive.
|
|
59
|
+
# @return [Boolean] true if the connection is alive
|
|
60
|
+
def alive?
|
|
61
|
+
@link_thread.alive?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Waits for the mpv connection to terminate.
|
|
65
|
+
# @param timeout [Numeric] optional timeout in seconds
|
|
66
|
+
# @return [Boolean] whether the connection was terminated
|
|
67
|
+
def wait(timeout=nil)
|
|
68
|
+
!!@link_thread.join(timeout)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Terminates the mpv communication.
|
|
72
|
+
# @return [void]
|
|
73
|
+
def release!
|
|
74
|
+
@link_thread.exit
|
|
75
|
+
@link_thread.join
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Sends a `quit` command to shut down the `mpv` process and
|
|
79
|
+
# releases the communication with it.
|
|
80
|
+
# @return [Boolean] true if the `quit` command was sent successfully
|
|
81
|
+
def quit!
|
|
82
|
+
command!("quit")
|
|
83
|
+
@link_thread.join
|
|
84
|
+
true
|
|
85
|
+
rescue
|
|
86
|
+
@link_thread.exit
|
|
87
|
+
@link_thread.join
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Sends a command to the `mpv` process.
|
|
92
|
+
# @param args [Array] the individual command arguments to send
|
|
93
|
+
# @raise [MPV::TimeoutError] if the request has timed out
|
|
94
|
+
# @raise [MPV::ConnectionError] in case of any socket error
|
|
95
|
+
# @raise [MPV::ReleasedConnectionError] if there is no connection anymore
|
|
96
|
+
# @return [Reply] mpv's response struct
|
|
97
|
+
# @example
|
|
98
|
+
# client.command("loadfile", "mymovie.mp4", "append-play").success? # => true
|
|
99
|
+
# client.command("asdfgh", "myparam").success? # => false
|
|
100
|
+
# client.command("asdfgh", "myparam").error # => "invalid parameter"
|
|
101
|
+
def command(*args)
|
|
102
|
+
raise ReleasedConnectionError unless alive?
|
|
103
|
+
@reply_queue.open(next_id) do |queue|
|
|
104
|
+
payload = { command: args, request_id: queue.id, async: true }
|
|
105
|
+
@cmd_mutex.synchronize{ @socket.puts(JSON.fast_generate(payload)) }
|
|
106
|
+
queue.pop
|
|
107
|
+
end
|
|
108
|
+
rescue IOError, SystemCallError => exc
|
|
109
|
+
raise ConnectionError, exc
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Sends a command to the `mpv` process and then checks if it was successful.
|
|
113
|
+
# @param args [Array] the individual command arguments to send
|
|
114
|
+
# @raise [MPV::TimeoutError] if the request has timed out
|
|
115
|
+
# @raise [MPV::ConnectionError] in case of any socket error
|
|
116
|
+
# @raise [MPV::ReleasedConnectionError] if there is no connection anymore
|
|
117
|
+
# @raise [MPV::ReplyError] if the response is unsuccessful
|
|
118
|
+
# @return [Object] mpv's response data value
|
|
119
|
+
# @example
|
|
120
|
+
# client.command!("loadfile", "mymovie.mp4", "append-play") # => nil
|
|
121
|
+
# client.command!("asdfgh", "myparam") # => raises MPV::ReplyError
|
|
122
|
+
def command!(*args)
|
|
123
|
+
command(*args).data!
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Retrieves a property from the `mpv` process.
|
|
127
|
+
# @param name [String] the property name (e.g.: volume)
|
|
128
|
+
# @return [Reply] mpv's response struct
|
|
129
|
+
# @example
|
|
130
|
+
# client.get_property("pause").data # => true
|
|
131
|
+
# client.get_property("volume").data # => 100.0
|
|
132
|
+
# client.get_property("asdfgh").data # => nil
|
|
133
|
+
# client.get_property("asdfgh").error # => "property not found"
|
|
134
|
+
def get_property(name)
|
|
135
|
+
command("get_property", name)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Retrieves a property from the `mpv` process and then confirms success.
|
|
139
|
+
# @param name [String] the property name (e.g.: volume)
|
|
140
|
+
# @raise [MPV::ReplyError] if the response is unsuccessful
|
|
141
|
+
# @return [Object] mpv's response data value
|
|
142
|
+
# @example
|
|
143
|
+
# client.get_property!("pause") # => true
|
|
144
|
+
# client.get_property!("volume") # => 100.0
|
|
145
|
+
# client.get_property!("asdfgh") # => raises MPV::ReplyError
|
|
146
|
+
def get_property!(name)
|
|
147
|
+
get_property(name).data!
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Sends a property change to the `mpv` process.
|
|
151
|
+
# @param name [String] the property name (e.g.: volume)
|
|
152
|
+
# @param value [Object] property value
|
|
153
|
+
# @return [Reply] mpv's response struct
|
|
154
|
+
# @example
|
|
155
|
+
# client.set_property("pause", true).success? # => true
|
|
156
|
+
# client.set_property("volume", 100.0).success? # => true
|
|
157
|
+
# client.set_property("asdfgh", 42).success? # => false
|
|
158
|
+
# client.set_property("asdfgh", 42).error # => "property not found"
|
|
159
|
+
def set_property(name, value)
|
|
160
|
+
command("set_property", name, value)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Sends a property change to the `mpv` process and then confirms success.
|
|
164
|
+
# @param name [String] the property name (e.g.: volume)
|
|
165
|
+
# @param value [Object] property value
|
|
166
|
+
# @raise [MPV::ReplyError] if the response is unsuccessful
|
|
167
|
+
# @return [Object] mpv's response data value
|
|
168
|
+
# @example
|
|
169
|
+
# client.set_property!("pause", true) # => nil
|
|
170
|
+
# client.set_property!("volume", 100.0) # => nil
|
|
171
|
+
# client.set_property!("asdfgh", 42) # => raises MPV::ReplyError
|
|
172
|
+
def set_property!(name, value)
|
|
173
|
+
set_property(name, value).data!
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Observes property changes.
|
|
177
|
+
# @param name [String] name of the property to observe
|
|
178
|
+
# @yield [ObserverEvent] event triggered by a property change
|
|
179
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
180
|
+
# @return [Integer] the observer id to use with unobserve_property
|
|
181
|
+
# @example
|
|
182
|
+
# client.observe_property("volume") do |event|
|
|
183
|
+
# puts "the new volume is #{event.data}"
|
|
184
|
+
# end
|
|
185
|
+
def observe_property(name, &block)
|
|
186
|
+
id = next_id
|
|
187
|
+
@property_observers[id] = block
|
|
188
|
+
command!("observe_property", id, name)
|
|
189
|
+
id
|
|
190
|
+
rescue
|
|
191
|
+
@property_observers.delete(id)
|
|
192
|
+
raise
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Unobserves property changes.
|
|
196
|
+
# @param id [Integer] the return value of #observe_property
|
|
197
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
198
|
+
# @return [void]
|
|
199
|
+
def unobserve_property(id)
|
|
200
|
+
command!("unobserve_property", id)
|
|
201
|
+
@property_observers.delete(id)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Registers an event listener.
|
|
205
|
+
# @param name [String] event name to listen for (defaults to all)
|
|
206
|
+
# @yield [Event] called with the event descriptor object
|
|
207
|
+
# @return [void]
|
|
208
|
+
def register_event_listener(name="", &block)
|
|
209
|
+
@event_listeners[name] = block
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Unregisters an event listener.
|
|
213
|
+
# @param name [String] name of the listened event (defaults to all)
|
|
214
|
+
# @return [void]
|
|
215
|
+
def unregister_event_listener(name="")
|
|
216
|
+
@event_listeners.delete(name)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Registers a client-message handler.
|
|
220
|
+
# @param message [String] the script-message identifier
|
|
221
|
+
# @yield [Array] called with the arguments passed to client-message
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @example
|
|
224
|
+
# client.register_message_handler("cool-message") do |a, b|
|
|
225
|
+
# puts "hello #{a}-#{b}" # => hello, mikuru-chan
|
|
226
|
+
# end
|
|
227
|
+
#
|
|
228
|
+
# client.command("script-message", "cool-message", "mikuru", "chan")
|
|
229
|
+
def register_message_handler(message, &block)
|
|
230
|
+
@message_handlers[message] = block
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Unregisters a client-message handler.
|
|
234
|
+
# @param message [String] the client-message identifier
|
|
235
|
+
# @return [void]
|
|
236
|
+
def unregister_message_handler(message)
|
|
237
|
+
@message_handlers.delete(message)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Registers a keybinding section.
|
|
241
|
+
# @param keys [Array<String>] key names to bind (in input.conf format)
|
|
242
|
+
# @param section [String] optional custom section name
|
|
243
|
+
# @param flags [Symbol] optional key binding priority (default: :default)
|
|
244
|
+
# :default lets user bindings win, while :force overrides them
|
|
245
|
+
# @yield [KeyEvent] the keybinding event
|
|
246
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
247
|
+
# @return [String] section name (for unregister_keybindings)
|
|
248
|
+
# @example
|
|
249
|
+
# client.register_keybindings("a", "b") do |key_event|
|
|
250
|
+
# key_event.hold? # => true
|
|
251
|
+
# key_event.key # => "b"
|
|
252
|
+
# end
|
|
253
|
+
#
|
|
254
|
+
# client.command("keypress", "b")
|
|
255
|
+
def register_keybindings(*keys, section: nil, flags: :default, &block)
|
|
256
|
+
section ||= "sect_#{next_id}"
|
|
257
|
+
contents = keys.flatten.map{ |key| "#{key} script-binding #{client_name}/#{section}" }
|
|
258
|
+
@keybinding_mutex.synchronize do
|
|
259
|
+
@keybinding_handlers[section] = block
|
|
260
|
+
command!("define-section", section, contents.join("\n"), flags)
|
|
261
|
+
command!("enable-section", section)
|
|
262
|
+
rescue
|
|
263
|
+
@keybinding_handlers.delete(section)
|
|
264
|
+
raise
|
|
265
|
+
end
|
|
266
|
+
section
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Unregisters a keybinding section.
|
|
270
|
+
# @param section [String] section name
|
|
271
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
272
|
+
# @return [void]
|
|
273
|
+
def unregister_keybindings(section)
|
|
274
|
+
@keybinding_mutex.synchronize do
|
|
275
|
+
command!("disable-section", section)
|
|
276
|
+
command!("define-section", section, "")
|
|
277
|
+
@keybinding_handlers.delete(section)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Creates a new message and adds it to the OSD.
|
|
282
|
+
# @param text [String, Ass::Text] message
|
|
283
|
+
# @param timeout [Numeric] optional timeout in seconds to autoremove the message
|
|
284
|
+
# If omitted or non-positive, the message is not scheduled for automatic removal.
|
|
285
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
286
|
+
# @return [Integer] message id
|
|
287
|
+
def create_osd_message(text, timeout: nil)
|
|
288
|
+
id = next_id
|
|
289
|
+
@osd_messages[id] = nil, nil
|
|
290
|
+
edit_osd_message(id, text, timeout: timeout)
|
|
291
|
+
id
|
|
292
|
+
rescue
|
|
293
|
+
@osd_messages.delete(id)
|
|
294
|
+
raise
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Edits one of the messages on the OSD.
|
|
298
|
+
# @param id [Integer] message id
|
|
299
|
+
# @param text [String, Ass::Text] message
|
|
300
|
+
# @param timeout [Numeric] optional timeout in seconds to autoremove the message
|
|
301
|
+
# If omitted, the timeout remains unchanged. If non-positive, autoremove is disabled.
|
|
302
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
303
|
+
# @return [Boolean] true if the message with the given id still existed
|
|
304
|
+
def edit_osd_message(id, text, timeout: nil)
|
|
305
|
+
text = Ass::Text.new(text) unless text.is_a?(Ass::Text)
|
|
306
|
+
@osd_mutex.synchronize do
|
|
307
|
+
if record = @osd_messages[id]
|
|
308
|
+
record[0] = text.to_script
|
|
309
|
+
render_osd_messages
|
|
310
|
+
if timeout
|
|
311
|
+
if timeout.positive?
|
|
312
|
+
unless record[1]&.reschedule(timeout)
|
|
313
|
+
record[1] = task = Concurrent::ScheduledTask.execute(timeout) do
|
|
314
|
+
@osd_mutex.synchronize do
|
|
315
|
+
if @osd_messages.dig(id, 1) == task
|
|
316
|
+
@osd_messages.delete(id)
|
|
317
|
+
render_osd_messages
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
elsif record[1]
|
|
323
|
+
record[1].cancel
|
|
324
|
+
record[1] = nil
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
!!record
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Deletes one of the messages on the OSD.
|
|
333
|
+
# @param id [Integer] message id
|
|
334
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
335
|
+
# @return [void]
|
|
336
|
+
def delete_osd_message(id)
|
|
337
|
+
@osd_mutex.synchronize do
|
|
338
|
+
if record = @osd_messages.delete(id)
|
|
339
|
+
record[1]&.cancel
|
|
340
|
+
render_osd_messages
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Deletes all messages on the OSD.
|
|
347
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
348
|
+
# @return [void]
|
|
349
|
+
def clear_osd_messages
|
|
350
|
+
@osd_mutex.synchronize do
|
|
351
|
+
unless @osd_messages.empty?
|
|
352
|
+
@osd_messages.each_value{ |(_, scheduler)| scheduler&.cancel }
|
|
353
|
+
@osd_messages.clear
|
|
354
|
+
render_osd_messages
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Enters a modal mode, similar to Vim's modes, but exits after a
|
|
361
|
+
# single keypress or when the exit key is pressed.
|
|
362
|
+
# @param message [String] message to show while the modal mode is active
|
|
363
|
+
# @param keys [Array<String>] the keys to bind (in input.conf format)
|
|
364
|
+
# @param exit_key [String] the key to exit modal mode (in input.conf format)
|
|
365
|
+
# @raise [MPV::ReplyError] if mpv rejects an underlying command
|
|
366
|
+
# @yield [KeyEvent] the keybinding event
|
|
367
|
+
# @return [String] section name (for unregister_keybindings)
|
|
368
|
+
# @example
|
|
369
|
+
# client.register_keybindings(%w[d]) do
|
|
370
|
+
# client.enter_modal_mode("really delete?", %w[y n]) do |key_event|
|
|
371
|
+
# key_event.press? # => true
|
|
372
|
+
# key_event.key # => "y"
|
|
373
|
+
# end
|
|
374
|
+
# end
|
|
375
|
+
def enter_modal_mode(message, keys, exit_key: "ESC", &block)
|
|
376
|
+
osd_id = create_osd_message(message)
|
|
377
|
+
register_keybindings(*keys, exit_key, flags: :force) do |event|
|
|
378
|
+
delete_osd_message(osd_id) rescue nil
|
|
379
|
+
unregister_keybindings(event.section) rescue nil
|
|
380
|
+
block.call(event) if block_given? && event.key != exit_key
|
|
381
|
+
end
|
|
382
|
+
rescue
|
|
383
|
+
delete_osd_message(osd_id) rescue nil
|
|
384
|
+
raise
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Registers a shutdown callback for notification or cleanup.
|
|
388
|
+
# @yield [Boolean] called after the mpv connection is closed
|
|
389
|
+
# with true if the server initiated the socket closure
|
|
390
|
+
# @return [void]
|
|
391
|
+
def register_shutdown_callback(&block)
|
|
392
|
+
@shutdown_callback = block
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Unregisters the current shutdown callback.
|
|
396
|
+
# @return [void]
|
|
397
|
+
def unregister_shutdown_callback
|
|
398
|
+
@shutdown_callback = nil
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
private
|
|
402
|
+
Reply = Struct.new(:data, :error, keyword_init: true) do
|
|
403
|
+
def success?
|
|
404
|
+
error == "success"
|
|
405
|
+
end
|
|
406
|
+
def error?
|
|
407
|
+
!success?
|
|
408
|
+
end
|
|
409
|
+
def data!
|
|
410
|
+
raise ReplyError, error if error?
|
|
411
|
+
data
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
Event = Struct.new(:name, :raw, keyword_init: true)
|
|
416
|
+
|
|
417
|
+
ObserverEvent = Struct.new(:name, :data, :raw, keyword_init: true)
|
|
418
|
+
|
|
419
|
+
KeyEvent = Struct.new(:section, :state, :key, :key2, :raw) do
|
|
420
|
+
def up?
|
|
421
|
+
state == "u-"
|
|
422
|
+
end
|
|
423
|
+
def down?
|
|
424
|
+
state == "d-"
|
|
425
|
+
end
|
|
426
|
+
def hit?
|
|
427
|
+
state == "p-"
|
|
428
|
+
end
|
|
429
|
+
def repeat?
|
|
430
|
+
state == "r-"
|
|
431
|
+
end
|
|
432
|
+
def press?
|
|
433
|
+
hit? or down?
|
|
434
|
+
end
|
|
435
|
+
def hold?
|
|
436
|
+
press? or repeat?
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
RESPONSE_TIMEOUT = 6 # sec
|
|
441
|
+
|
|
442
|
+
# Don't use 0, mpv observers don't work otherwise, it's treated as nil.
|
|
443
|
+
MIN_REQ_ID = 1
|
|
444
|
+
|
|
445
|
+
# The virtual machine's maximum Fixnum. Equals 2^62 on CRuby compiled
|
|
446
|
+
# on a 64-bit machine, and up to 2^64 on VMs like JRuby. mpv goes up
|
|
447
|
+
# to 2^64, so this is a safe value to make the atomic counter roll over.
|
|
448
|
+
MAX_REQ_ID = (2**(0.size * 8 - 2) - 1)
|
|
449
|
+
|
|
450
|
+
def next_id
|
|
451
|
+
@request_id.update{ |current| current < MAX_REQ_ID ? current + 1 : MIN_REQ_ID }
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def client_name
|
|
455
|
+
@client_name ||= command!("client_name")
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def render_osd_messages
|
|
459
|
+
if @osd_messages.empty?
|
|
460
|
+
command!("osd-overlay", 0, "none", "")
|
|
461
|
+
else
|
|
462
|
+
scripts = @osd_messages.values.map(&:first)
|
|
463
|
+
command!("osd-overlay", 0, "ass-events", scripts.join("\n"))
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def dispatch_property_observer(event)
|
|
468
|
+
if observer = @property_observers[event.raw["id"]]
|
|
469
|
+
property = event.raw.slice("name", "data")
|
|
470
|
+
@event_queue.push([observer, ObserverEvent.new(**property, raw: event.raw)])
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def dispatch_message_handler(event)
|
|
475
|
+
message, *args = event.raw["args"]
|
|
476
|
+
|
|
477
|
+
if message == "key-binding" and handler = @keybinding_handlers[args.first]
|
|
478
|
+
@event_queue.push([handler, KeyEvent.new(*args.values_at(0..3), event.raw)])
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
handler = @message_handlers[message]
|
|
482
|
+
@event_queue.push([handler, *args]) if handler
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def dispatch_event_listener(event)
|
|
486
|
+
listener = @event_listeners[event.name]
|
|
487
|
+
@event_queue.push([listener, event]) if listener
|
|
488
|
+
|
|
489
|
+
listener = @event_listeners[""]
|
|
490
|
+
@event_queue.push([listener, event]) if listener
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def dispatch_event_handlers(event)
|
|
494
|
+
case event.name
|
|
495
|
+
when "property-change" then dispatch_property_observer(event)
|
|
496
|
+
when "client-message" then dispatch_message_handler(event)
|
|
497
|
+
end
|
|
498
|
+
dispatch_event_listener(event)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def link_loop
|
|
502
|
+
while line = @socket.gets and line.chomp!
|
|
503
|
+
response = JSON.parse(line) rescue {}
|
|
504
|
+
if response.key?("event")
|
|
505
|
+
event = Event.new(name: response["event"], raw: response)
|
|
506
|
+
dispatch_event_handlers(event)
|
|
507
|
+
elsif response.key?("request_id")
|
|
508
|
+
reply = Reply.new(response.slice("data", "error"))
|
|
509
|
+
@reply_queue.push(response["request_id"], reply)
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
eof = !line
|
|
513
|
+
rescue
|
|
514
|
+
ensure
|
|
515
|
+
@socket.close if @path
|
|
516
|
+
if @shutdown_callback
|
|
517
|
+
@event_queue.push([@link_thread.method(:join)])
|
|
518
|
+
@event_queue.push([@shutdown_callback, !!eof])
|
|
519
|
+
end
|
|
520
|
+
@event_queue.close
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def event_loop
|
|
524
|
+
while event = @event_queue.pop
|
|
525
|
+
event.shift.call(*event) rescue nil
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MPV
|
|
4
|
+
# Generic error class, all exceptions inherit from this.
|
|
5
|
+
class Error < RuntimeError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Raised when an mpv reply is unsuccessful.
|
|
9
|
+
class ReplyError < Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Raised when a request has timed out.
|
|
13
|
+
class TimeoutError < Error
|
|
14
|
+
def initialize
|
|
15
|
+
super "Request timed out"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when some socket communication error occurs.
|
|
20
|
+
# The connection will be released.
|
|
21
|
+
class ConnectionError < Error
|
|
22
|
+
def initialize(msg=nil)
|
|
23
|
+
super(msg.is_a?(String) ? msg :
|
|
24
|
+
"Socket communication error occurred with the mpv#{": #{msg}" if msg}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Raised when the connection to mpv has already been released.
|
|
29
|
+
class ReleasedConnectionError < ConnectionError
|
|
30
|
+
def initialize
|
|
31
|
+
super "Connection released, further communication with mpv is not possible"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raised when the mpv startup has failed.
|
|
36
|
+
class StartupError < Error
|
|
37
|
+
def initialize(msg=nil)
|
|
38
|
+
super("Failed to start mpv#{": #{msg}" if msg}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raised when no usable `mpv` executable is available.
|
|
43
|
+
class NotAvailableError < Error
|
|
44
|
+
def initialize
|
|
45
|
+
super "No usable mpv executable available"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Raised when `mpv` doesn't support a requested option.
|
|
50
|
+
class UnsupportedOptionError < Error
|
|
51
|
+
def initialize(option)
|
|
52
|
+
super "The installed mpv does not support the #{option} option"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "exceptions"
|
|
4
|
+
|
|
5
|
+
module MPV
|
|
6
|
+
# Holds state to handle request/response lifecycle for mpv's ipc protocol.
|
|
7
|
+
class MultiQueue
|
|
8
|
+
class Reader
|
|
9
|
+
attr_reader :id
|
|
10
|
+
|
|
11
|
+
def initialize(table, id)
|
|
12
|
+
table.instance_variables.each do |var|
|
|
13
|
+
instance_variable_set(var, table.instance_variable_get(var))
|
|
14
|
+
end
|
|
15
|
+
@mutex.synchronize{ @table[@id = id] ||= [] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Closes this queue.
|
|
19
|
+
def close
|
|
20
|
+
@mutex.synchronize{ @resource.broadcast if @table.delete(@id) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Pops a value from this queue.
|
|
24
|
+
# @param timeout [Numeric] optional (non-default) timeout in seconds
|
|
25
|
+
# @raise [MPV::TimeoutError] if timed out
|
|
26
|
+
# @raise [KeyError] if already closed
|
|
27
|
+
# @return [Object] the value
|
|
28
|
+
def pop(timeout=@timeout)
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
31
|
+
while (queue = @table.fetch(@id)).empty?
|
|
32
|
+
raise TimeoutError if timeout&.negative?
|
|
33
|
+
@resource.wait(@mutex, timeout)
|
|
34
|
+
timeout &&= deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
end
|
|
36
|
+
queue.pop
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Creates a new MPV::MultiQueue instance.
|
|
42
|
+
# @param timeout [Numeric] default timeout for pops in seconds
|
|
43
|
+
def initialize(timeout=nil)
|
|
44
|
+
@timeout = timeout
|
|
45
|
+
@table = Hash.new
|
|
46
|
+
@mutex = Mutex.new
|
|
47
|
+
@resource = ConditionVariable.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Opens a new queue for a certain unique id.
|
|
51
|
+
# @param id [Integer] the unique queue id
|
|
52
|
+
# @yield [Reader] if given, yields the queue reader
|
|
53
|
+
# then closes it and returns the block’s result.
|
|
54
|
+
# @return [Object] new queue reader, or the block's result if given
|
|
55
|
+
def open(id, &block)
|
|
56
|
+
reader = Reader.new(self, id)
|
|
57
|
+
block ? block.call(reader) : reader
|
|
58
|
+
ensure
|
|
59
|
+
reader.close if block and reader
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pushes a value to a certain queue.
|
|
63
|
+
# @param id [Integer] the unique queue id
|
|
64
|
+
# @param value [Object] the value
|
|
65
|
+
# @return [Boolean] whether the push was successful
|
|
66
|
+
def push(id, value)
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
if queue = @table[id]
|
|
69
|
+
queue.push(value)
|
|
70
|
+
@resource.broadcast
|
|
71
|
+
end
|
|
72
|
+
!!queue
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|