├── .editorconfig ├── .github └── dependabot.yml ├── .gitignore ├── .rubocop.yml ├── .yardopts ├── LICENSE ├── README.md ├── lib ├── mpv.rb └── mpv │ ├── client.rb │ ├── exceptions.rb │ ├── server.rb │ ├── session.rb │ └── utils.rb └── mpv.gemspec /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{rb,sh}] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | doc/ 3 | .yardoc/ 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Global style enforcement. 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.3 5 | 6 | Style/StringLiterals: 7 | EnforcedStyle: double_quotes 8 | 9 | Style/StringLiteralsInInterpolation: 10 | EnforcedStyle: double_quotes 11 | 12 | Style/TrailingCommaInArrayLiteral: 13 | EnforcedStyleForMultiline: comma 14 | 15 | Style/TrailingCommaInHashLiteral: 16 | EnforcedStyleForMultiline: comma 17 | 18 | Style/FormatString: 19 | EnforcedStyle: percent 20 | 21 | Style/DoubleNegation: 22 | Enabled: false 23 | 24 | Style/RescueModifier: 25 | Enabled: false 26 | 27 | Style/TrailingUnderscoreVariable: 28 | Enabled: false 29 | 30 | Metrics/LineLength: 31 | Max: 100 32 | 33 | Metrics/MethodLength: 34 | Max: 15 35 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --tag command:"muzak command" --tag cmdexample:"example invocation" --no-private --markup-provider=redcarpet --markup=markdown - *.md LICENSE 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 William Woodruff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-mpv 2 | ======== 3 | 4 | [![Gem Version](https://badge.fury.io/rb/mpv.svg)](https://badge.fury.io/rb/mpv) 5 | 6 | A ruby library for controlling mpv processes. 7 | 8 | ### Installation 9 | 10 | ```bash 11 | $ gem install mpv 12 | ``` 13 | 14 | ### Example 15 | 16 | For full documentation, please see the 17 | [RubyDocs](http://www.rubydoc.info/gems/mpv/). 18 | 19 | ```ruby 20 | # this will be called every time mpv sends an event back over the socket 21 | def something_happened(event) 22 | puts "look ma! a callback: #{event.to_s}" 23 | end 24 | 25 | session = MPV::Session.new # contains both a MPV::Server and a MPV::Client 26 | session.callbacks << method(:something_happened) 27 | session.get_property "pause" 28 | session.command "get_version" 29 | session.command "loadlist", "my_huge_playlist.txt", "append" 30 | session.quit! 31 | ``` 32 | -------------------------------------------------------------------------------- /lib/mpv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "mpv/exceptions" 4 | require_relative "mpv/utils" 5 | require_relative "mpv/client" 6 | require_relative "mpv/server" 7 | require_relative "mpv/session" 8 | 9 | # The toplevel namespace for ruby-mpv. 10 | module MPV 11 | # The current version of ruby-mpv. 12 | VERSION = "3.0.1" 13 | end 14 | -------------------------------------------------------------------------------- /lib/mpv/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | require "json" 5 | 6 | module MPV 7 | # Represents a connection to a mpv process that has been spawned 8 | # with an IPC socket. 9 | # @see https://mpv.io/manual/stable/#json-ipc 10 | # MPV's IPC docs 11 | # @see https://mpv.io/manual/master/#properties 12 | # MPV's property docs 13 | class Client 14 | # @return [String] the path of the socket used to communicate with mpv 15 | attr_reader :socket_path 16 | 17 | # @return [Array] callback procs that will be invoked 18 | # whenever mpv emits an event 19 | attr_accessor :callbacks 20 | 21 | # @param path [String] the domain socket for communication with mpv 22 | def initialize(path) 23 | @socket_path = path 24 | 25 | @socket = UNIXSocket.new(@socket_path) 26 | @alive = true 27 | 28 | @callbacks = [] 29 | 30 | @command_queue = Queue.new 31 | @result_queue = Queue.new 32 | @event_queue = Queue.new 33 | 34 | @command_thread = Thread.new { pump_commands! } 35 | @results_thread = Thread.new { pump_results! } 36 | @events_thread = Thread.new { dispatch_events! } 37 | end 38 | 39 | # @return [Boolean] whether or not the player is currently active 40 | # @note When false, most methods will cease to function. 41 | def alive? 42 | @alive 43 | end 44 | 45 | # Sends a command to the mpv process. 46 | # @param args [Array] the individual command arguments to send 47 | # @return [Hash] mpv's response to the command 48 | # @example 49 | # client.command "loadfile", "mymovie.mp4", "append-play" 50 | def command(*args) 51 | return unless alive? 52 | 53 | payload = { 54 | "command" => args, 55 | } 56 | 57 | @command_queue << JSON.generate(payload) 58 | 59 | @result_queue.pop 60 | end 61 | 62 | # Sends a property change to the mpv process. 63 | # @param args [Array] the individual property arguments to send 64 | # @return [Hash] mpv's response 65 | # @example 66 | # client.set_property "pause", true 67 | def set_property(*args) 68 | return unless alive? 69 | 70 | command "set_property", *args 71 | end 72 | 73 | # Retrieves a property from the mpv process. 74 | # @param args [Array] the individual property arguments to send 75 | # @return [Object] the value of the property 76 | # @example 77 | # client.get_property "pause" # => true 78 | def get_property(*args) 79 | return unless alive? 80 | 81 | command("get_property", *args)["data"] 82 | end 83 | 84 | # Terminates the mpv process. 85 | # @return [void] 86 | # @note this object becomes garbage once this method is run 87 | def quit! 88 | command "quit" if alive? 89 | ensure 90 | @alive = false 91 | @socket = nil 92 | File.delete(@socket_path) if File.exist?(@socket_path) 93 | end 94 | 95 | private 96 | 97 | # Pumps commands from the command queue to the socket. 98 | # @api private 99 | def pump_commands! 100 | loop do 101 | begin 102 | @socket.puts(@command_queue.pop) 103 | rescue StandardError # the player is deactivating 104 | @alive = false 105 | Thread.exit 106 | end 107 | end 108 | end 109 | 110 | # Distributes results in a nonterminating loop. 111 | # @api private 112 | def pump_results! 113 | loop do 114 | distribute_results! 115 | end 116 | end 117 | 118 | # Distributes results to the event and result queues. 119 | # @api private 120 | def distribute_results! 121 | response = JSON.parse(@socket.readline) 122 | 123 | if response["event"] 124 | @event_queue << response 125 | else 126 | @result_queue << response 127 | end 128 | rescue StandardError 129 | @alive = false 130 | Thread.exit 131 | end 132 | 133 | # Takes events from the event queue and dispatches them to callbacks. 134 | # @api private 135 | def dispatch_events! 136 | loop do 137 | event = @event_queue.pop 138 | 139 | callbacks.each do |callback| 140 | Thread.new do 141 | callback.call event 142 | end 143 | end 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/mpv/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MPV 4 | # A generic error class for ruby-mpv. 5 | class MPVError < RuntimeError 6 | end 7 | 8 | # Raised when `mpv` cannot be executed. 9 | class MPVNotAvailableError < MPVError 10 | def initialize 11 | super "Could not find an mpv binary to execute in the system path" 12 | end 13 | end 14 | 15 | # Raised when `mpv` doesn't support a requested flag. 16 | class MPVUnsupportedFlagError < MPVError 17 | def initialize(flag) 18 | super "Installed mpv doesn't support the #{flag} flag" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mpv/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tempfile" 4 | 5 | module MPV 6 | # Represents an active mpv process. 7 | class Server 8 | # @return [Array] the command-line arguments used when spawning mpv 9 | attr_reader :args 10 | 11 | # @return [String] the path to the socket used by this mpv process 12 | attr_reader :socket_path 13 | 14 | # @return [Fixnum] the process id of the mpv process 15 | attr_reader :pid 16 | 17 | # @return [Boolean] whether `mpv` is executable within the system path 18 | def self.available? 19 | Utils.which?("mpv") 20 | end 21 | 22 | # @return [Boolean] whether `mpv` supports the given flag 23 | # @note returns false if `mpv` is not available 24 | def self.flag?(flag) 25 | return false unless available? 26 | 27 | # MPV allows flags to be suffixed with =yes or =no, but doesn't 28 | # include these variations in their list. They also allow a --no- 29 | # prefix that isn't included in the list, so we normalize these out. 30 | # Additionally, we need to remove trailing arguments. 31 | normalized_flag = flag.sub(/^--no-/, "--").sub(/=\S*/, "") 32 | 33 | flags = `mpv --list-options`.split.select { |s| s.start_with?("--") } 34 | flags.include?(normalized_flag) 35 | end 36 | 37 | # Ensures that a binary named `mpv` can be executed. 38 | # @raise [MPVNotAvailableError] if no `mpv` executable in the system path 39 | def self.ensure_available! 40 | raise MPVNotAvailableError unless available? 41 | end 42 | 43 | # Ensures that that the `mpv` being executed supports the given flag. 44 | # @raise [MPVNotAvailableError] if no `mpv` executable in the system path 45 | # @raise [MPVUnsupportedFlagError] if `mpv` does not support the given flag 46 | def self.ensure_flag!(flag) 47 | ensure_available! 48 | raise MPVUnsupportedFlagError, flag unless flag?(flag) 49 | end 50 | 51 | # @param path [String] the path of the socket to be created 52 | # (defaults to a tmpname in `/tmp`) 53 | # @param user_args [Array] additional arguments to use when 54 | # spawning mpv 55 | def initialize(path: File.join("/tmp", Utils.tmpsock), user_args: []) 56 | @socket_path = path 57 | @args = [ 58 | "--idle", 59 | "--terminal=no", 60 | "--input-ipc-server=%s" % { path: @socket_path }, 61 | ].concat(user_args).uniq 62 | 63 | @args.each { |arg| self.class.ensure_flag! arg } 64 | 65 | @pid = Process.spawn("mpv", *@args) 66 | end 67 | 68 | # @return [Boolean] whether or not the mpv process is running 69 | def running? 70 | !!@pid && Process.waitpid(@pid, Process::WNOHANG).nil? 71 | rescue Errno::ECHILD 72 | false 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/mpv/session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | module MPV 6 | # Represents a combined mpv "server" and "client" communicating over 7 | # JSON IPC. 8 | class Session 9 | extend Forwardable 10 | 11 | # @return [String] the path of the socket being used for communication 12 | attr_reader :socket_path 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 | # @param path [String] the path of the socket to create 21 | # (defaults to a tmpname in `/tmp`) 22 | # @param user_args [Array] additional arguments to use when 23 | # spawning mpv 24 | def initialize(path: File.join("/tmp", Utils.tmpsock), user_args: []) 25 | @socket_path = path 26 | 27 | @server = Server.new(path: @socket_path, user_args: user_args) 28 | 29 | sleep 0.1 until File.exist?(@socket_path) 30 | 31 | @client = Client.new(@socket_path) 32 | end 33 | 34 | # @!method running? 35 | # @return (see MPV::Server#running?) 36 | # @see MPV::Server#running? 37 | def_delegators :@server, :running? 38 | 39 | # @!method callbacks 40 | # @return (see MPV::Client#callbacks) 41 | # @see MPV::Client#callbacks 42 | def_delegators :@client, :callbacks 43 | 44 | # @!method quit! 45 | # @return (see MPV::Client#quit!) 46 | # @see MPV::Client#quit! 47 | def_delegators :@client, :quit! 48 | 49 | # @!method command 50 | # @return (see MPV::Client#command) 51 | # @see MPV::Client#command 52 | def_delegators :@client, :command 53 | 54 | # @!method get_property 55 | # @return (see MPV::Client#get_property) 56 | # @see MPV::Client#get_property 57 | def_delegators :@client, :get_property 58 | 59 | # @!method set_property 60 | # @return (see MPV::Client#set_property) 61 | # @see MPV::Client#set_property 62 | def_delegators :@client, :set_property 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/mpv/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "English" 4 | 5 | module MPV 6 | # Various utility methods for ruby-mpv. 7 | module Utils 8 | # Tests whether the given utility is available in the system path. 9 | # @param util [String] the utility to test 10 | # @return [Boolean] whether or not the utility is available 11 | # @api private 12 | def self.which?(util) 13 | ENV["PATH"].split(File::PATH_SEPARATOR).any? do |path| 14 | File.executable?(File.join(path, util)) 15 | end 16 | end 17 | 18 | def self.tmpsock 19 | t = Time.now.strftime("%Y%m%d") 20 | "mpv#{t}-#{$PROCESS_ID}-#{rand(0x100000000).to_s(36)}.sock" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /mpv.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/mpv" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "mpv" 7 | s.version = MPV::VERSION 8 | s.summary = "mpv - A ruby library for controlling mpv processes." 9 | s.description = "A library for creating and controlling mpv instances." 10 | s.authors = ["William Woodruff"] 11 | s.email = "william@tuffbizz.com" 12 | s.files = Dir["LICENSE", "*.md", ".yardopts", "lib/**/*"] 13 | s.required_ruby_version = ">= 2.3.0" 14 | s.homepage = "https://github.com/woodruffw/ruby-mpv" 15 | s.license = "MIT" 16 | end 17 | --------------------------------------------------------------------------------