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.
@@ -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