easy_mplayer 1.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.
- data/.document +5 -0
- data/.gitignore +2 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/easy_mplayer.gemspec +67 -0
- data/examples/basic.rb +66 -0
- data/examples/callbacks.rb +98 -0
- data/examples/inherit_class.rb +98 -0
- data/examples/minimal.rb +15 -0
- data/lib/easy_mplayer.rb +17 -0
- data/lib/easy_mplayer/callback.rb +54 -0
- data/lib/easy_mplayer/commands.rb +83 -0
- data/lib/easy_mplayer/errors.rb +106 -0
- data/lib/easy_mplayer/helpers.rb +91 -0
- data/lib/easy_mplayer/main.rb +180 -0
- data/lib/easy_mplayer/worker.rb +354 -0
- metadata +95 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
class MPlayer
|
2
|
+
class Callback # :nodoc:all
|
3
|
+
def initialize(callback_options)
|
4
|
+
@block = callback_options[:block]
|
5
|
+
@type = callback_options[:type]
|
6
|
+
@scope = callback_options[:scope]
|
7
|
+
end
|
8
|
+
|
9
|
+
def run!(args)
|
10
|
+
unless @block.nil?
|
11
|
+
case @type
|
12
|
+
when :instance then @block.call(*args)
|
13
|
+
when :class then @scope.instance_exec(*args, &@block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class CallbackList < Array # :nodoc:all
|
20
|
+
attr_reader :name
|
21
|
+
|
22
|
+
def initialize(list_name)
|
23
|
+
@name = list_name.to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
def register(opts)
|
27
|
+
push Callback.new(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
def run!(args)
|
31
|
+
each do |x|
|
32
|
+
x.run!(args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def all
|
38
|
+
@all ||= Hash.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def find(name)
|
42
|
+
all[name.to_sym] ||= new(name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def register(opts)
|
46
|
+
find(opts[:name]).register(opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def run!(name, args)
|
50
|
+
find(name).run!(args)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class MPlayer
|
2
|
+
class Command # :nodoc:all
|
3
|
+
class << self
|
4
|
+
def cmdlist_raw
|
5
|
+
@cmdlist_raw ||= `mplayer -input cmdlist`.split(/\n/)
|
6
|
+
end
|
7
|
+
|
8
|
+
def list
|
9
|
+
@list ||= returning Hash.new do |hsh|
|
10
|
+
cmdlist_raw.map do |line|
|
11
|
+
cmd, *opts = line.split(/\s+/)
|
12
|
+
hsh[cmd.to_sym] = new(cmd, opts)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(name)
|
18
|
+
list[name.to_sym]
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!(args)
|
22
|
+
cmd = args.shift
|
23
|
+
obj = find(cmd)
|
24
|
+
raise BadCallName.new(cmd, args) unless obj
|
25
|
+
obj.validate!(args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :cmd, :names, :opts, :max, :min
|
30
|
+
|
31
|
+
def initialize(command_name, opt_list)
|
32
|
+
@cmd = command_name
|
33
|
+
@min = 0
|
34
|
+
@max = opt_list.length
|
35
|
+
@names = opt_list
|
36
|
+
@opts = opt_list.map do |opt|
|
37
|
+
@min += 1 if opt[0,1] != '['
|
38
|
+
case opt
|
39
|
+
when 'Integer', '[Integer]' then :int
|
40
|
+
when 'Float', '[Float]' then :float
|
41
|
+
when 'String', '[String]' then :string
|
42
|
+
else raise "Unknown cmd option type: #{opt}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def usage
|
48
|
+
"#{cmd}(" + names.join(", ") + ")"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
usage
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class} \"#{usage}\">"
|
57
|
+
end
|
58
|
+
|
59
|
+
def convert_arg_type(val, type)
|
60
|
+
begin
|
61
|
+
case type
|
62
|
+
when :int then Integer(val)
|
63
|
+
when :float then Float(val)
|
64
|
+
when :string then val.to_s
|
65
|
+
end
|
66
|
+
rescue
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate!(args)
|
72
|
+
len = args.length
|
73
|
+
raise BadCallArgs.new(self, args, "not enough args") if len < min
|
74
|
+
raise BadCallArgs.new(self, args, "too many args") if len > max
|
75
|
+
returning Array.new do |new_args|
|
76
|
+
args.each_with_index do |x,i|
|
77
|
+
new_args.push convert_arg_type(x, opts[i]) or
|
78
|
+
raise BadCallArgs.new(self, args, "type mismatch")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class MPlayer
|
2
|
+
module Error # :nodoc:all
|
3
|
+
# all errors thrown form this library will be of this type
|
4
|
+
class MPlayerError < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
class StartupError < MPlayerError
|
8
|
+
attr_reader :path
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
str = "Missing startup requirement!\n"
|
12
|
+
str += "File \"#{path}\" does not exist!\n" unless File.exists?(path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(path)
|
16
|
+
@path = path
|
17
|
+
super(to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NoPlayerFound < StartupError
|
22
|
+
def to_s
|
23
|
+
str = super
|
24
|
+
str += "File \"#{path}\" is not executable!\n" unless File.executable?(path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NoTargetPath < StartupError
|
29
|
+
def to_s
|
30
|
+
str = super
|
31
|
+
str += "file \"#{path}\" is not readable!\n" unless File.readable?(path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# some unknown error having to do with the streams/threads that
|
36
|
+
# connect us to the mplayer process
|
37
|
+
class BadStream < MPlayerError
|
38
|
+
end
|
39
|
+
|
40
|
+
# tried to change to a different level of output that doesn't exist
|
41
|
+
class BadMsgType < MPlayerError
|
42
|
+
attr_reader :badtype
|
43
|
+
|
44
|
+
def valid_types # :nodoc:
|
45
|
+
DEBUG_MESSAGE_TYPES.keys.inspect
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s # :nodoc:
|
49
|
+
"Bad debug message type \"#{badtype}\"\nValid types " + valid_types
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(type)
|
53
|
+
@badtype = type
|
54
|
+
super(to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# an error in sending a command to mplayer over its slave-mode API
|
59
|
+
class BadCall < MPlayerError
|
60
|
+
attr_reader :cmd, :args
|
61
|
+
|
62
|
+
# a type-prototype of how we attempted the mplayer API call
|
63
|
+
def called_as
|
64
|
+
"#{cmd}(" + args.map do |x|
|
65
|
+
x.class
|
66
|
+
end.join(", ") + ")"
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
"\nBad MPlayer call: #{called_as}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(command, called_args)
|
74
|
+
@cmd = command
|
75
|
+
@args = called_args
|
76
|
+
super(to_s)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# tried to pass a slave-mode command to mplayer, but the call didn't
|
81
|
+
# match the API prototype mplayer itself provided
|
82
|
+
class BadCallArgs < BadCall
|
83
|
+
attr_reader :msg, :usage
|
84
|
+
|
85
|
+
def to_s # :nodoc:
|
86
|
+
super + " - #{msg}\nusage: #{usage}"
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def initialize(command, called_args, message)
|
91
|
+
@msg = message
|
92
|
+
@usage = command.usage
|
93
|
+
super(command.cmd, called_args)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# not a valid command name
|
98
|
+
class BadCallName < BadCall
|
99
|
+
def to_s
|
100
|
+
super + "\nNo such command \"#{cmd.inspect}\""
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
include Error
|
106
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class MPlayer
|
2
|
+
# a blocking version of #play for trivial uses. It returns only
|
3
|
+
# after the mplayer process finally terminates itself
|
4
|
+
def play_to_end
|
5
|
+
play
|
6
|
+
sleep 1 while running?
|
7
|
+
end
|
8
|
+
|
9
|
+
# spawn the mplayer process, which also starts the media playing. It
|
10
|
+
# is requires that #opts[:path] point to a valid media file
|
11
|
+
def play
|
12
|
+
stop if running?
|
13
|
+
|
14
|
+
info "PLAY: #{opts[:path]}"
|
15
|
+
worker.startup!
|
16
|
+
end
|
17
|
+
|
18
|
+
# kill off the mplayer process. This invalidates any running media,
|
19
|
+
# though it can be restarted again with another call to #play
|
20
|
+
def stop
|
21
|
+
info "STOP!"
|
22
|
+
@worker.shutdown! if @worker
|
23
|
+
end
|
24
|
+
|
25
|
+
# pause playback if we are running
|
26
|
+
def pause
|
27
|
+
return if paused?
|
28
|
+
info "PAUSE!"
|
29
|
+
send_command :pause
|
30
|
+
@paused = true
|
31
|
+
callback! :pause, true
|
32
|
+
end
|
33
|
+
|
34
|
+
# opposite of #pause
|
35
|
+
def unpause
|
36
|
+
return unless paused?
|
37
|
+
info "UNPAUSE!"
|
38
|
+
send_command :pause
|
39
|
+
@paused = false
|
40
|
+
callback! :unpause, false
|
41
|
+
end
|
42
|
+
|
43
|
+
# use this instead of #pause or #unpause, and the flag will be
|
44
|
+
# toggled with each call
|
45
|
+
def pause_or_unpause
|
46
|
+
paused? ? unpause : pause
|
47
|
+
end
|
48
|
+
|
49
|
+
# Seek to an absolute position in a file, by percent of the total size.
|
50
|
+
# requires a float argument, that is <tt>(0.0 <= percent <= 100.0)</tt>
|
51
|
+
def seek_to_percent(percent)
|
52
|
+
return if percent.to_i == @stats[:position]
|
53
|
+
percent = percent.to_f
|
54
|
+
percent = 0.0 if percent < 0
|
55
|
+
percent = 100.0 if percent > 100
|
56
|
+
info "SEEK TO: #{percent}%"
|
57
|
+
send_command :seek, percent, 1
|
58
|
+
end
|
59
|
+
|
60
|
+
# seek to an absolute position in a file, by seconds. requires a
|
61
|
+
# float between 0.0 and the length (in seconds) of the file being played.
|
62
|
+
def seek_to_time(seconds)
|
63
|
+
info "SEEK TO: #{seconds} seconds"
|
64
|
+
send_command :seek, seconds, 1
|
65
|
+
end
|
66
|
+
|
67
|
+
# seek by a relative amount, in seconds. requires a float. Negative
|
68
|
+
# values rewind to a previous point.
|
69
|
+
def seek_by(amount)
|
70
|
+
info "SEEK BY: #{amount}"
|
71
|
+
send_command :seek, amount, 0
|
72
|
+
end
|
73
|
+
|
74
|
+
# seek forward a given number of seconds, or
|
75
|
+
# <tt>opts[:seek_size]</tt> seconds by default
|
76
|
+
def seek_forward(amount = opts[:seek_size])
|
77
|
+
seek_by(amount)
|
78
|
+
end
|
79
|
+
|
80
|
+
# seek backwards (rewind) by a given number of seconds, or
|
81
|
+
# <tt>opts[:seek_size]</tt> seconds by default. Note that a
|
82
|
+
# /positive/ value here rewinds!
|
83
|
+
def seek_reverse(amount = opts[:seek_size])
|
84
|
+
seek_by(-amount)
|
85
|
+
end
|
86
|
+
|
87
|
+
# reset back to the beginning of the file
|
88
|
+
def seek_start
|
89
|
+
seek_to_percent(0.0)
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
class MPlayer
|
2
|
+
# all of these can be overridden by passing them to #new
|
3
|
+
DEFAULT_OPTS = {
|
4
|
+
:program => '/usr/bin/mplayer',
|
5
|
+
:message_style => :info,
|
6
|
+
:seek_size => 10,
|
7
|
+
:select_wait_time => 1,
|
8
|
+
:thread_safe_callbacks => true
|
9
|
+
}
|
10
|
+
|
11
|
+
# the color_debug_message parameter sets we can switch
|
12
|
+
# between, for convenience. (flags for ColorDebugMessages)
|
13
|
+
DEBUG_MESSAGE_TYPES = {
|
14
|
+
:quiet => {
|
15
|
+
},
|
16
|
+
:error_only => {
|
17
|
+
:warn => true
|
18
|
+
},
|
19
|
+
:info => {
|
20
|
+
:warn => true,
|
21
|
+
:info => true
|
22
|
+
},
|
23
|
+
:debug => {
|
24
|
+
:warn => true,
|
25
|
+
:info => true,
|
26
|
+
:debug => true,
|
27
|
+
:class_only => false
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
attr_reader :callbacks, :stats, :opts
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def class_callbacks # :nodoc:
|
35
|
+
@@class_callbacks ||= Array.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# register a block with the named callback(s). This is the same
|
39
|
+
# as the instance-method, generally, but it uses instance_exec to
|
40
|
+
# give the block the same scope (the MPlayer instance)
|
41
|
+
def callback(*names, &block)
|
42
|
+
names.each do |name|
|
43
|
+
class_callbacks << {
|
44
|
+
:name => name,
|
45
|
+
:type => :class,
|
46
|
+
:block => block
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# create a new object. The option hash must have, at a minimum, a
|
53
|
+
# :path reference to the file we want to play, or an exception will
|
54
|
+
# be raised.
|
55
|
+
def initialize(new_opts=Hash.new)
|
56
|
+
@opts = DEFAULT_OPTS.merge(new_opts)
|
57
|
+
set_message_style opts[:message_style]
|
58
|
+
|
59
|
+
unless File.executable?(@opts[:program])
|
60
|
+
raise NoPlayerFound.new(@opts[:program])
|
61
|
+
end
|
62
|
+
unless @opts[:path] and File.readable?(new_opts[:path])
|
63
|
+
raise NoTargetPath.new(@opts[:path])
|
64
|
+
end
|
65
|
+
|
66
|
+
@stats = Hash.new
|
67
|
+
@callbacks = Hash.new
|
68
|
+
@worker = nil
|
69
|
+
|
70
|
+
self.class.class_callbacks.each do |opts|
|
71
|
+
opts[:scope] = self
|
72
|
+
CallbackList.register opts
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
callback :update_stat do |*args|
|
77
|
+
update_stat *args
|
78
|
+
end
|
79
|
+
|
80
|
+
callback :file_error do
|
81
|
+
warn "File error!"
|
82
|
+
stop!
|
83
|
+
end
|
84
|
+
|
85
|
+
callback :played_time do |played_time|
|
86
|
+
update_stat :played_seconds, played_time.to_i
|
87
|
+
total = stats[:total_time]
|
88
|
+
if total and total != 0.0
|
89
|
+
pos = (100 * played_time / total)
|
90
|
+
update_stat :raw_position, pos
|
91
|
+
update_stat :position, pos.to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
callback :startup do
|
96
|
+
callback! :play
|
97
|
+
end
|
98
|
+
|
99
|
+
callback :shutdown do
|
100
|
+
@worker = nil
|
101
|
+
callback! :stop
|
102
|
+
end
|
103
|
+
|
104
|
+
# can be any of:
|
105
|
+
# :quiet Supperss all output!
|
106
|
+
# :error_only Off except for errors
|
107
|
+
# :info Also show information messages
|
108
|
+
# :debug Heavy debug output (spammy)
|
109
|
+
def set_message_style(type)
|
110
|
+
hsh = DEBUG_MESSAGE_TYPES[type.to_sym] or
|
111
|
+
raise BadMsgType.new(type.inspect)
|
112
|
+
hsh = hsh.dup
|
113
|
+
hsh[:debug] ||= false
|
114
|
+
hsh[:info] ||= false
|
115
|
+
hsh[:warn] ||= false
|
116
|
+
hsh[:class_only] ||= true
|
117
|
+
hsh[:prefix_only] ||= false
|
118
|
+
ColorDebugMessages.global_debug_flags(hsh)
|
119
|
+
opts[:message_style] = type
|
120
|
+
end
|
121
|
+
|
122
|
+
def inspect # :nodoc:
|
123
|
+
vals = [['running', running?],
|
124
|
+
['paused', paused?]]
|
125
|
+
vals << ['info', stats.inspect] if running?
|
126
|
+
"#<#{self.class} " + vals.map do |x|
|
127
|
+
x.first + '=' + x.last.to_s
|
128
|
+
end.join(' ') + '>'
|
129
|
+
end
|
130
|
+
|
131
|
+
# call an entire callback chain, passing in a list of args
|
132
|
+
def callback!(name, *args) # :nodoc:
|
133
|
+
#puts "CALLBACK! #{name.inspect} #{args.inspect}"
|
134
|
+
CallbackList.run!(name, args)
|
135
|
+
end
|
136
|
+
|
137
|
+
# register a function into each of the named callback chains
|
138
|
+
def callback(*names, &block)
|
139
|
+
names.each do |name|
|
140
|
+
CallbackList.register :name => name, :block => block, :type => :instance
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# true if we are running, yet the media has stopped
|
145
|
+
def paused?
|
146
|
+
@paused
|
147
|
+
end
|
148
|
+
|
149
|
+
# true if the mplayer process is active and running
|
150
|
+
def running?
|
151
|
+
!!@worker and @worker.ok?
|
152
|
+
end
|
153
|
+
|
154
|
+
# pipe a command to mplayer via slave mode
|
155
|
+
def send_command(*args)
|
156
|
+
worker.send_command(*args)
|
157
|
+
end
|
158
|
+
|
159
|
+
def worker # :nodoc:
|
160
|
+
create_worker if @worker.nil?
|
161
|
+
@worker
|
162
|
+
end
|
163
|
+
|
164
|
+
def create_worker # :nodoc:
|
165
|
+
callback! :creating_worker
|
166
|
+
@worker = Worker.new(self)
|
167
|
+
@stats = Hash.new
|
168
|
+
@paused = false
|
169
|
+
callback! :worker_running
|
170
|
+
end
|
171
|
+
|
172
|
+
def update_stat(name, newval) # :nodoc:
|
173
|
+
name = name.to_sym
|
174
|
+
if @stats[name] != newval
|
175
|
+
debug "STATS[:#{name}] -> #{newval.inspect}"
|
176
|
+
@stats[name] = newval
|
177
|
+
callback! name, newval
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|