puremotion 0.0.1

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,490 @@
1
+ module PureMotion
2
+
3
+ class Media
4
+
5
+ event :analysed
6
+
7
+ attr_accessor :file
8
+
9
+ @analysed = false
10
+
11
+ @unknown_format = false
12
+ @invalid_file = false
13
+
14
+ @streams = nil
15
+
16
+ def self.analyse(input, options = {})
17
+ return self.new(input)
18
+ end
19
+
20
+ def analysed?
21
+ @analysed
22
+ end
23
+
24
+ def initialize(file)
25
+ @file = file
26
+
27
+ @output = ''
28
+ raise ArgumentError, "File does not exist" unless File.exists? @file
29
+
30
+ @ffmpeg = Tools::FFmpeg.new :options => "-i #{file}"
31
+
32
+ @ffmpeg.line + lambda do |ffmpeg, line|
33
+
34
+ end
35
+
36
+ @ffmpeg.complete + lambda do | ffmpeg, out |
37
+ @output = @ffmpeg.output.join("\n")
38
+
39
+ metadata = /(Input \#.*)\n.+\n\Z/m.match(@output)
40
+
41
+ @unknown_format = has(/Unknown format/i)
42
+ @invalid_file = has(/Duration: N\/A|bitrate: N\/A/im)
43
+
44
+ @analysed = true
45
+ analysed(true)
46
+ end
47
+
48
+ end
49
+
50
+ # Raw output of FFmpeg
51
+ #
52
+ # Returns:
53
+ #
54
+ # * Nil: If the video is still being analysed - unless the parameter :so_far is passed
55
+ # * String: Raw output
56
+ def output(opt)
57
+
58
+ return nil unless valid?
59
+
60
+ if status(:analysing) then return nil unless opt == :so_far end
61
+
62
+ return output.join('\n')
63
+
64
+ end
65
+
66
+ def transcode(params)
67
+ return Transcode::Transcode.new(self, params)
68
+ end
69
+
70
+ # Returns:
71
+ # * nil if not yet analysed
72
+ # * false if the media is an unknown format
73
+ # * false if the file is not media or is corrupt
74
+ # * true otherwise
75
+ def valid?
76
+ return nil unless @analysed
77
+ return false if @unknown_format || @invalid_file
78
+ has /Input #\d*.\d*/
79
+ end
80
+
81
+ def invalid?
82
+ !valid?
83
+ end
84
+
85
+
86
+ def unknown_format?
87
+ @unknown_format
88
+ end
89
+
90
+ # If true then either:
91
+ # * The media is a valid format but corrupted and cannot be read
92
+ # or
93
+ # * The file isn't media!
94
+ def invalid_file?
95
+ @invalid_file
96
+ end
97
+
98
+ # Container bitrate:
99
+ #
100
+ # Returns:
101
+ # * Nil: If the bitrate could not be detected
102
+ # * String: "xxx kb/s" if _fmt_ is :raw
103
+ # * Integer: bitrate in kb/s otherwise
104
+ def bitrate(fmt = nil)
105
+ br = bitrate_match
106
+ return nil if br.nil?
107
+
108
+ mbr = {
109
+ :value => br[1].to_i,
110
+ :unit => br[2]
111
+ }
112
+
113
+ return mbr[:value] unless fmt == :raw
114
+
115
+ br
116
+
117
+ end
118
+
119
+ # Boolean: Whether media has a video stream or not
120
+ def video?
121
+ return nil if not @analysed
122
+ return false if not valid?
123
+ !video_match.nil?
124
+ end
125
+
126
+ # Boolean: Whether media has an audio stream or not
127
+ def audio?
128
+ return nil if not @analysed
129
+ has /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(.*?),\s*(\d*)x(\d*)/
130
+ end
131
+
132
+ # Returns a hash with all sorts of details about the first video stream
133
+ # Nil if no video stream
134
+ def video
135
+ return nil unless video?
136
+ {
137
+ :codec => detail(:video, :codec),
138
+ :colorspace => detail(:video, :colorspace),
139
+ :resolution => {
140
+ :width => detail(:video, :resolution, :width),
141
+ :height => detail(:video, :resolution, :height)
142
+ },
143
+ :fps => detail(:video, :fps),
144
+ :bitrate => detail(:video, :bitrate, :raw),
145
+ :duration => detail(:duration, :seconds)
146
+ }
147
+ end
148
+
149
+ def audio
150
+ return nil unless audio?
151
+ {
152
+ :codec => detail(:audio, :codec),
153
+ :sample_rate => detail(:audio, :sample, :rate),
154
+ :sample_size => detail(:audio, :sample, :size),
155
+ :channels => detail(:audio, :channels),
156
+ :bitrate => detail(:audio, :bitrate)
157
+ }
158
+ end
159
+
160
+ # Big messy function that allows things like:
161
+ # * detail :video, :resolution, :height
162
+ # * detail :video, :fps
163
+ # * detail :video, :duration
164
+ def detail(*args)
165
+
166
+ return nil unless valid?
167
+
168
+ case args[0]
169
+ when :video
170
+
171
+ # Main Video handler
172
+
173
+ case args[1]
174
+ when :codec
175
+
176
+ # Find video codec
177
+ return video_match[2]
178
+
179
+ when :colorspace
180
+
181
+ # Find colorspace
182
+ return video_match[3]
183
+
184
+ when :resolution
185
+
186
+ # Handle resolution
187
+
188
+ case args[2]
189
+ when :width
190
+
191
+ # Frame width
192
+ return video_match[4]
193
+
194
+ when :height
195
+
196
+ # Frame height
197
+ return video_match[5]
198
+
199
+ end
200
+
201
+ # Default resolution output format
202
+ return video_match[4] + "x" + video_match[5]
203
+
204
+ when :fps
205
+
206
+ # Frame Rate
207
+ fps = video_frame_rate_match[1].to_f
208
+
209
+ # Handle undected frame rates
210
+ if fps == 0 then fps = nil end
211
+
212
+ return fps
213
+
214
+ when :bitrate
215
+
216
+ # Captue bitrate via regex
217
+ br = video_bitrate_match[1].to_i
218
+
219
+ # Decide if bitrate is in kb/s or b/s
220
+ case video_bitrate_match[2]
221
+ when 'b/s'
222
+
223
+ # If in b/s divide to get kb/s
224
+ return (br / 1024).to_i
225
+
226
+ end
227
+
228
+ # Otherwise send raw bitrate
229
+ return br
230
+
231
+ when :duration
232
+
233
+ # Redirect to overall file duration
234
+ return detail(:duration, args[1])
235
+
236
+ end
237
+
238
+ # End video section
239
+
240
+ return nil
241
+
242
+ when :audio
243
+
244
+ # Audio section
245
+
246
+ case args[1]
247
+ when :codec
248
+
249
+ # Find the codec
250
+ codec = audio_match[2]
251
+
252
+ if codec =~ /\dx[\d a-f]*/ then
253
+ codec = :unidentifed
254
+ end
255
+
256
+ return codec
257
+
258
+ when :sample
259
+
260
+ case args[2]
261
+ when :rate
262
+ return audio_match[3].to_i
263
+ end
264
+
265
+ return nil
266
+
267
+ when :channels
268
+
269
+ case audio_match[5]
270
+ when 'stereo'
271
+ return 2
272
+ when 'mono'
273
+ return 1
274
+ end
275
+
276
+ return nil
277
+ when :bitrate
278
+ return audio_bitrate_match[1].to_i
279
+ end
280
+
281
+ return nil
282
+
283
+ when :duration
284
+
285
+ d = dur
286
+ secs = (d[:h] * 60 * 60) + (d[:m] * 60) + (d[:s]) * 1.00
287
+
288
+ case args[1]
289
+ when :seconds
290
+ return secs
291
+ when :frames
292
+ return (secs * video_frame_rate_match[1].to_f).to_i
293
+ end
294
+
295
+ return d[:raw]
296
+
297
+ end
298
+
299
+ nil
300
+ end
301
+
302
+ # * Float: Frame rate of video stream
303
+ # * Nil: If no video stream, undectable or invalid media
304
+ def fps
305
+ return nil unless valid?
306
+ return nil unless video?
307
+ begin
308
+ return video_frame_rate_match[1].to_f
309
+ rescue
310
+ return nil
311
+ end
312
+ end
313
+
314
+ private
315
+
316
+ def bitrate_match
317
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
318
+ end
319
+
320
+ def audio_match
321
+ return nil unless valid?
322
+
323
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Audio:\s*(.*?),\s*([0-9\.]*) (\w*),\s*([a-zA-Z:]*)/.match(@output)
324
+
325
+ match
326
+ end
327
+
328
+ def audio_bitrate_match
329
+ return nil if not valid?
330
+
331
+ match = /Stream\s*.*?[,|:|\(|\[].*?\s*Audio:\s*.*?,\s*[0-9\.]* \w*,\s*[a-zA-Z:]*,[^,]*,\s*(\d*)\s*([a-zA-Z]*\/s)/.match(@output)
332
+
333
+ match
334
+ end
335
+
336
+ def video_match
337
+ return nil unless valid?
338
+
339
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(.*?),\s*(\d*)x(\d*)/.match(@output)
340
+
341
+ /#
342
+
343
+ 1 => stream id
344
+ 2 => codec
345
+ 3 => colorspace
346
+ 4 => width
347
+ 5 => height
348
+
349
+
350
+ /
351
+
352
+ match
353
+ end
354
+
355
+ def video_bitrate_match
356
+ match = /Stream\s*.*?[,|:|\(|\[].*?\s*Video:\s*.*?,\s*.*?,\s*\d*x\d*[^,]*,\s*([0-9\.]+)\s*(.*\/.),/.match(@output)
357
+ match
358
+ end
359
+
360
+ def video_frame_rate_match
361
+ match = /Stream\s*.*?[,|:|\(|\[].*?\s*Video:\s\S*,\s\S*,\s\d+x\d+[^,]+,\s\S*\s\S*,\s([^,\s]*)/.match(@output)
362
+
363
+ match
364
+ end
365
+
366
+ def raw_duration
367
+ return nil unless valid?
368
+
369
+ /Duration:\s*([0-9\:\.]+),/.match(@output)[1]
370
+ end
371
+
372
+ def dur
373
+ return nil unless valid?
374
+
375
+ bits = /(\d*):(\d{2}):(\d{2}.\d*)/.match(raw_duration)
376
+ {
377
+ :raw => bits[0],
378
+ :h => bits[1].to_i,
379
+ :m => bits[2].to_i,
380
+ :s => bits[3].to_f
381
+ }
382
+ end
383
+
384
+ def has(regexp)
385
+ if @output =~ regexp then
386
+ true
387
+ else
388
+ false
389
+ end
390
+ end
391
+
392
+ def find(regexp)
393
+ m = regexp.match(@output)
394
+ return m[1] if m
395
+ nil
396
+ end
397
+
398
+ end
399
+
400
+ class Stream
401
+
402
+ def initialize(raw)
403
+ @raw = raw
404
+ end
405
+
406
+ def kind
407
+ m = /:\s*(\S*):/.match(@raw)
408
+ m[1]
409
+ end
410
+
411
+ end
412
+
413
+ class Input
414
+
415
+ attr_accessor :streams
416
+
417
+ def initialize(raw)
418
+ @raw = raw
419
+ @streams = []
420
+ detect_streams
421
+ end
422
+
423
+ def container
424
+ /Input \#\d+\,\s*(\S+),\s*from/.match(@raw)[1]
425
+ end
426
+
427
+ def file
428
+ /from '(\S+)'/.match(@raw)[1]
429
+ end
430
+
431
+ protected
432
+
433
+ def detect_streams
434
+ @raw.each_line do |line|
435
+ @streams << Stream.new(line) if line =~ /\s*Stream/
436
+ end
437
+ end
438
+
439
+ end
440
+
441
+ class Analysis
442
+
443
+ attr_accessor :info, :inputs
444
+
445
+ def initialize(options = {})
446
+ analyse
447
+ @inputs[0] unless @inputs.empty?
448
+ end
449
+
450
+ def analyse
451
+ @inputs = []
452
+ @fio = IO.popen('ffmpeg -i C:/ffmpeg/test.wmv 2>&1')
453
+
454
+ prev_indent = -1
455
+ indent = 0
456
+ lines = []
457
+
458
+ begin
459
+ f = @fio.gets
460
+
461
+ indent = f.length - f.lstrip.length
462
+
463
+ if indent < prev_indent then
464
+ add(lines.join)
465
+ lines = []
466
+ end
467
+
468
+ prev_indent = indent
469
+
470
+ #puts "Indent: " + indent.to_s + " - " + f
471
+
472
+ if f =~ /Input #\d*/ then
473
+
474
+ end
475
+
476
+ lines << f
477
+
478
+ end while not @fio.eof?
479
+
480
+ end
481
+
482
+ def add(block)
483
+ if m = /^Input #(\d+),/.match(block) then
484
+ @inputs[m[0].to_i] = Input.new block
485
+ end
486
+ end
487
+
488
+ end
489
+
490
+ end
@@ -0,0 +1,12 @@
1
+ name: iPod
2
+ desc: Encodes a video suitable for iPod
3
+ recipe:
4
+ video:
5
+ codec: libx264
6
+ bitrate: 512
7
+ resolution: 320x240
8
+ fps: 20
9
+ audio:
10
+ codec: aac
11
+ bitrate: 128
12
+ channels: mono
@@ -0,0 +1,153 @@
1
+ # Author:: Jared Kuolt (mailto:me@superjared.com)
2
+ # Copyright:: Copyright (c) 2009 Jared Kuolt
3
+ # License:: MIT License
4
+ #
5
+ # See README.rdoc[link:files/README_rdoc.html] for usage
6
+ #
7
+ require 'logger'
8
+ require 'timeout'
9
+
10
+ module PureMotion
11
+ class Thread
12
+ # The Thread object, brah
13
+ attr_reader :thread
14
+ # If the Thread takes a poopie...
15
+ attr_reader :exception
16
+ # An identifier
17
+ attr_accessor :label
18
+
19
+ # Create a new RobustThread (see README)
20
+ def initialize(opts={}, &block)
21
+ self.class.send :init_exit_handler
22
+ args = (opts[:args] or [])
23
+ self.class.send :do_before_init
24
+ @thread = ::Thread.new(*args) do |*targs|
25
+ begin
26
+ self.class.send :do_before_yield
27
+ block.call(*targs)
28
+ self.class.send :do_after_yield
29
+ rescue Exception => e
30
+ @exception = e
31
+ self.class.send :handle_exception, e
32
+ end
33
+ self.class.log "#{self.label.inspect} exited cleanly"
34
+ end
35
+ self.label = opts[:label] || @thread.inspect
36
+ self.class.group << self
37
+ end
38
+
39
+ ## Class methods and attributes
40
+ class << self
41
+ attr_accessor :logger, :say_goodnight, :exit_handler_initialized, :callbacks
42
+ VALID_CALLBACKS = [:before_init, :before_yield, :after_yield, :after_join, :before_exit]
43
+
44
+ # Logger object (see README)
45
+ def logger
46
+ @logger ||= Logger.new(STDOUT)
47
+ end
48
+
49
+ # Simple log interface
50
+ def log(msg, level=:info)
51
+ #self.logger.send level, "#{self}: " + msg
52
+ end
53
+
54
+ # The collection of RobustThread objects
55
+ def group
56
+ @group ||= []
57
+ end
58
+
59
+ # Loop an activity and exit it cleanly (see README)
60
+ def loop(opts={}, &block)
61
+ sleep_seconds = opts.delete(:seconds) || 2
62
+ self.new(opts) do |*args|
63
+ Kernel.loop do
64
+ break if self.say_goodnight
65
+ block.call(*args)
66
+ # We want to sleep for the right amount of time, but we also don't
67
+ # want to wait until the sleep is done if our exit handler has been
68
+ # called so we iterate over a loop, sleeping only 0.1 and checking
69
+ # each iteration whether we need to die, and the timeout is a noop
70
+ # indicating we need to continue.
71
+ begin
72
+ Timeout.timeout(sleep_seconds) do
73
+ Kernel.loop do
74
+ break if self.say_goodnight
75
+ sleep 0.1
76
+ end
77
+ end
78
+ rescue Timeout::Error
79
+ # OK
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # Set exception handler
86
+ def exception_handler(&block)
87
+ unless block.arity == 1
88
+ raise ArgumentError, "Bad arity for exception handler. It may only accept a single argument"
89
+ end
90
+ @exception_handler = block
91
+ end
92
+
93
+ # Add a callback
94
+ public
95
+ def add_callback(sym, &block)
96
+ sym = sym.to_sym
97
+ raise ArgumentError, "Invalid callback #{sym.inspect}" unless VALID_CALLBACKS.include? sym
98
+ self.callbacks ||= {}
99
+ self.callbacks[sym] ||= []
100
+ self.callbacks[sym] << block
101
+ end
102
+
103
+ private
104
+ # Calls exception handler if set (see RobustThread.exception_handler)
105
+ def handle_exception(exc)
106
+ if @exception_handler.is_a? Proc
107
+ @exception_handler.call(exc)
108
+ else
109
+ log("Unhandled exception:\n#{exc.message} " \
110
+ "(#{exc.class}): \n\t#{exc.backtrace.join("\n\t")}", :error)
111
+ end
112
+ end
113
+
114
+ # Sets up the exit_handler unless exit_handler_initialized
115
+ def init_exit_handler
116
+ return if self.exit_handler_initialized
117
+ self.say_goodnight = false
118
+ at_exit do
119
+ self.say_goodnight = true
120
+ begin
121
+ self.group.each do |rt|
122
+ log "waiting on #{rt.label.inspect}" if rt.thread.alive?
123
+ rt.thread.join
124
+ rt.class.send :do_after_join
125
+ end
126
+ self.send :do_before_exit
127
+ log "exited cleanly"
128
+ rescue Interrupt
129
+ log "prematurely killed by interrupt!", :error
130
+ end
131
+ end
132
+ self.exit_handler_initialized = true
133
+ end
134
+
135
+ def perform_callback(sym)
136
+ raise ArgumentError, "Cannot perform invalid callback #{sym.inspect}" unless VALID_CALLBACKS.include? sym
137
+ return unless self.callbacks and self.callbacks[sym]
138
+ self.callbacks[sym].reverse.each do |callback|
139
+ callback.call
140
+ end
141
+ end
142
+
143
+ # Performs callback, if possible
144
+ def method_missing(sym, *args)
145
+ if sym.to_s =~ /^do_(.*)$/
146
+ perform_callback($1.to_sym)
147
+ else
148
+ raise NoMethodError, "RobustThread method_missing: #{sym.inspect}"
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end