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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "concurrent"
5
+
6
+ require_relative "utils"
7
+ require_relative "exceptions"
8
+
9
+ module MPV
10
+ # Represents an active `mpv` process started with an IPC socket.
11
+ # @note Resources are released by calling `#wait` after termination.
12
+ # At exit, all running `mpv` processes are automatically stopped.
13
+ class Server
14
+ # @return [Array<String>] command-line options used when spawning `mpv`
15
+ attr_reader :options
16
+
17
+ # @return [String] the path to the currently used `mpv`
18
+ attr_reader :mpv_path
19
+
20
+ # @return [String] the path to the IPC socket used by this `mpv` process
21
+ attr_reader :ipc_path
22
+
23
+ @@existing_servers = Concurrent::Array.new
24
+ at_exit do
25
+ existing_servers = @@existing_servers.to_a
26
+ existing_servers.each{ |server| server.kill! }
27
+ existing_servers.each{ |server| server.wait }
28
+ end
29
+
30
+ # Creates a new MPV::Server instance and starts the `mpv` process.
31
+ # @param bind [String] the path of the IPC socket to be used
32
+ # for the communication (defaults to a temporary path in `/tmp`)
33
+ # @param mpv [String] the path of the `mpv` executable
34
+ # (defaults to searching in system `$PATH`)
35
+ # @param options [Array<String>] additional initial options to pass
36
+ # to the `mpv` process
37
+ # @param chdir [String] custom working directory for `mpv`
38
+ # @raise [MPV::StartupError] if the `mpv` process fails to start
39
+ # @raise [MPV::NotAvailableError] if no `mpv` executable is available
40
+ # @raise [MPV::UnsupportedOptionError] if a given `mpv` option is not supported
41
+ def initialize(bind: nil, mpv: nil, options: [], chdir: nil)
42
+ @mpv_path = mpv || Utils.which("mpv")
43
+ raise NotAvailableError unless @mpv_path
44
+
45
+ @ipc_path = bind || File.join("/tmp", Utils.tmpsock)
46
+ File.delete(@ipc_path) if File.exist?(@ipc_path)
47
+
48
+ @options = options.map{ |option| option.sub(/\A-{1,2}/, "") }
49
+ @options << "terminal=no" << "input-ipc-server=#{@ipc_path}" << "idle=yes"
50
+
51
+ stdout, status = Open3.capture2(@mpv_path, "--list-options") rescue nil
52
+ raise NotAvailableError unless status&.success?
53
+ available_options = stdout.scan(/^\s*--(\S+)/).map(&:first)
54
+ @options.each{ |option| ensure_option!(option, available_options) }
55
+
56
+ args = @options.map{ |option| "--#{option}" }
57
+ params = {pgroup: true}.merge(({chdir: chdir} if chdir).to_h)
58
+ @pid = Process.spawn(@mpv_path, *args, **params)
59
+
60
+ timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC) + STARTUP_TIMEOUT
61
+ until File.exist?(@ipc_path)
62
+ if status = Process.wait2(@pid, Process::WNOHANG)&.last
63
+ raise StartupError, "unexpected termination with " \
64
+ "#{status.to_s.delete_prefix("pid #{status.pid} ")}"
65
+ end
66
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > timeout
67
+ Process.kill(:KILL, -@pid)
68
+ Process.wait(@pid)
69
+ raise StartupError, "timed out"
70
+ end
71
+ sleep 0.02
72
+ end
73
+
74
+ @@existing_servers << self
75
+ end
76
+
77
+ # Checks whether the `mpv` process is still alive.
78
+ # @return [Boolean] true if the `mpv` process is alive
79
+ def alive?
80
+ @pid = nil if wait_pid(@pid, false)
81
+ !!@pid
82
+ end
83
+
84
+ # Blocks until the `mpv` process is terminated, then releases all resources.
85
+ # @return [void]
86
+ def wait
87
+ wait_pid(@pid, true)
88
+ File.delete(@ipc_path) rescue nil if @ipc_path
89
+ @pid = @ipc_path = nil
90
+ @@existing_servers.delete(self)
91
+ end
92
+
93
+ # Sends a signal to the `mpv` process.
94
+ # @param signal [Object] the name (String/Symbol) or code (Integer)
95
+ # of the signal to send
96
+ # @return [Boolean] true if the signal was sent successfully
97
+ def kill!(signal=:TERM)
98
+ pid = @pid
99
+ Process.kill(signal, -pid) if pid
100
+ !!pid
101
+ rescue Errno::ESRCH
102
+ false
103
+ end
104
+
105
+ private
106
+ STARTUP_TIMEOUT = 6 # sec
107
+
108
+ def ensure_option!(option, available_options)
109
+ # Options can be prefixed with `no-` (equivalent to a `=no` suffix),
110
+ # but these variations aren’t included in the list of supported options,
111
+ # so these need to be removed before checking.
112
+ opt = option.sub(/\A(?:no-)?(.*?)(?:=.*)?\z/, "\\1")
113
+ raise UnsupportedOptionError, opt unless available_options.include?(opt)
114
+ end
115
+
116
+ def wait_pid(pid, blocking)
117
+ !pid || !!Process.wait(pid, blocking ? 0 : Process::WNOHANG)
118
+ rescue Errno::ECHILD
119
+ true
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ require_relative "client"
6
+ require_relative "server"
7
+
8
+ module MPV
9
+ # Combines an MPV::Server and MPV::Client, connecting them to each other.
10
+ # @note Automatically releases resources after termination.
11
+ class Session
12
+ extend Forwardable
13
+
14
+ # @return [MPV::Server] the server object responsible for the mpv process
15
+ attr_reader :server
16
+
17
+ # @return [MPV::Client] the client communicating with mpv
18
+ attr_reader :client
19
+
20
+ # Creates a new MPV::Session instance containing an MPV::Server and an
21
+ # MPV::Client that are connected to each other.
22
+ # All parameters are passed to MPV::Server.
23
+ # @see MPV::Server::new
24
+ def initialize(**params)
25
+ @server = Server.new(**params)
26
+ @client = Client.new(@server.ipc_path)
27
+
28
+ @shutdown_callback = nil
29
+ @client.register_shutdown_callback do |eof|
30
+ @shutdown_callback.call(eof) if @shutdown_callback
31
+ ensure
32
+ @server.wait if Thread.main.alive?
33
+ end
34
+ rescue
35
+ if @server
36
+ @server.kill!
37
+ @server.wait
38
+ end
39
+ raise
40
+ end
41
+
42
+ # Registers a shutdown callback for notification or cleanup.
43
+ # @yield [Boolean] called with a boolean indicating whether
44
+ # the server closed the connection properly
45
+ # @return [void]
46
+ def register_shutdown_callback(&block)
47
+ @shutdown_callback = block
48
+ end
49
+
50
+ # Unregisters the current shutdown callback.
51
+ # @return [void]
52
+ def unregister_shutdown_callback
53
+ @shutdown_callback = nil
54
+ end
55
+
56
+ # Terminates the mpv process and waits for it.
57
+ # It releases all the resources.
58
+ # @return [Boolean] false if it was already terminated
59
+ def quit!
60
+ result = @client.quit! || @server.kill!
61
+ @server.wait
62
+ result
63
+ end
64
+
65
+ # @!method alive?
66
+ # @return (see MPV::Client#alive?)
67
+ # @see MPV::Client#alive?
68
+ def_delegators :@client, :alive?
69
+
70
+ # @!method wait
71
+ # @return (see MPV::Client#wait)
72
+ # @see MPV::Client#wait
73
+ def_delegators :@client, :wait
74
+
75
+ # @!method command
76
+ # @return (see MPV::Client#command)
77
+ # @see MPV::Client#command
78
+ def_delegators :@client, :command
79
+
80
+ # @!method command!
81
+ # @return (see MPV::Client#command!)
82
+ # @see MPV::Client#command!
83
+ def_delegators :@client, :command!
84
+
85
+ # @!method get_property
86
+ # @return (see MPV::Client#get_property)
87
+ # @see MPV::Client#get_property
88
+ def_delegators :@client, :get_property
89
+
90
+ # @!method get_property!
91
+ # @return (see MPV::Client#get_property!)
92
+ # @see MPV::Client#get_property!
93
+ def_delegators :@client, :get_property!
94
+
95
+ # @!method set_property
96
+ # @return (see MPV::Client#set_property)
97
+ # @see MPV::Client#set_property
98
+ def_delegators :@client, :set_property
99
+
100
+ # @!method set_property!
101
+ # @return (see MPV::Client#set_property!)
102
+ # @see MPV::Client#set_property!
103
+ def_delegators :@client, :set_property!
104
+
105
+ # @!method observe_property
106
+ # @return (see MPV::Client#observe_property)
107
+ # @see MPV::Client#observe_property
108
+ def_delegators :@client, :observe_property
109
+
110
+ # @!method unobserve_property
111
+ # @return (see MPV::Client#unobserve_property)
112
+ # @see MPV::Client#unobserve_property
113
+ def_delegators :@client, :unobserve_property
114
+
115
+ # @!method register_event_listener
116
+ # @return (see MPV::Client#register_event_listener)
117
+ # @see MPV::Client#register_event_listener
118
+ def_delegators :@client, :register_event_listener
119
+
120
+ # @!method unregister_event_listener
121
+ # @return (see MPV::Client#unregister_event_listener)
122
+ # @see MPV::Client#unregister_event_listener
123
+ def_delegators :@client, :unregister_event_listener
124
+
125
+ # @!method register_message_handler
126
+ # @return (see MPV::Client#register_message_handler)
127
+ # @see MPV::Client#register_message_handler
128
+ def_delegators :@client, :register_message_handler
129
+
130
+ # @!method unregister_message_handler
131
+ # @return (see MPV::Client#unregister_message_handler)
132
+ # @see MPV::Client#unregister_message_handler
133
+ def_delegators :@client, :unregister_message_handler
134
+
135
+ # @!method register_keybindings
136
+ # @return (see MPV::Client#register_keybindings)
137
+ # @see MPV::Client#register_keybindings
138
+ def_delegators :@client, :register_keybindings
139
+
140
+ # @!method unregister_keybindings
141
+ # @return (see MPV::Client#unregister_keybindings)
142
+ # @see MPV::Client#unregister_keybindings
143
+ def_delegators :@client, :unregister_keybindings
144
+
145
+ # @!method create_osd_message
146
+ # @return (see MPV::Client#create_osd_message)
147
+ # @see MPV::Client#create_osd_message
148
+ def_delegators :@client, :create_osd_message
149
+
150
+ # @!method edit_osd_message
151
+ # @return (see MPV::Client#edit_osd_message)
152
+ # @see MPV::Client#edit_osd_message
153
+ def_delegators :@client, :edit_osd_message
154
+
155
+ # @!method delete_osd_message
156
+ # @return (see MPV::Client#delete_osd_message)
157
+ # @see MPV::Client#delete_osd_message
158
+ def_delegators :@client, :delete_osd_message
159
+
160
+ # @!method clear_osd_messages
161
+ # @return (see MPV::Client#clear_osd_messages)
162
+ # @see MPV::Client#clear_osd_messages
163
+ def_delegators :@client, :clear_osd_messages
164
+
165
+ # @!method enter_modal_mode
166
+ # @return (see MPV::Client#enter_modal_mode)
167
+ # @see MPV::Client#enter_modal_mode
168
+ def_delegators :@client, :enter_modal_mode
169
+ end
170
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MPV
4
+ # Various utility methods for mpv-ipc.
5
+ module Utils
6
+ # Finds where the given utility is located in the system paths.
7
+ # @param name [String] name of the utility to find
8
+ # @return [String] full path of the utility
9
+ # @api private
10
+ def self.which(name)
11
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR).map{ |dir| File.join(dir, name) }
12
+ paths.find{ |path| File.file?(path) && File.executable?(path) }
13
+ end
14
+
15
+ def self.tmpsock
16
+ "mpv-ipc-#{$$}-#{Time.now.strftime("%Y%m%d%N")}-#{rand(36**8).to_s(36)}.sock"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MPV
4
+ # The current version of mpv-ipc.
5
+ VERSION = "5.0.0"
6
+ end
data/lib/mpv_ipc.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mpv_ipc/version"
4
+ require_relative "mpv_ipc/session"
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mpv-ipc
3
+ version: !ruby/object:Gem::Version
4
+ version: 5.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Péter Bakonyi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.6
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.6
33
+ description: |-
34
+ A Ruby interface to the mpv media player via its JSON IPC protocol.
35
+ It can start and manage mpv instances, connect to and control existing players, or be used to write Ruby scripts that run embedded within mpv.
36
+ email: bakonyi.peter@gmail.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - LICENSE
42
+ - README.md
43
+ - lib/mpv_ass.rb
44
+ - lib/mpv_ass/color.rb
45
+ - lib/mpv_ass/span.rb
46
+ - lib/mpv_ass/text.rb
47
+ - lib/mpv_ipc.rb
48
+ - lib/mpv_ipc/client.rb
49
+ - lib/mpv_ipc/exceptions.rb
50
+ - lib/mpv_ipc/multi_queue.rb
51
+ - lib/mpv_ipc/server.rb
52
+ - lib/mpv_ipc/session.rb
53
+ - lib/mpv_ipc/utils.rb
54
+ - lib/mpv_ipc/version.rb
55
+ homepage: https://github.com/BPeti/mpv-ipc
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ source_code_uri: https://github.com/BPeti/mpv-ipc
60
+ documentation_uri: https://www.rubydoc.info/gems/mpv-ipc
61
+ rubygems_mfa_required: 'true'
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 2.7.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 3.1.2
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: A simple interface to mpv via JSON IPC.
81
+ test_files: []