easy_mplayer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|