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,141 @@
1
+ module PureMotion::Tools
2
+
3
+ class FFmpeg
4
+
5
+ class Status
6
+
7
+ NOT_STARTED = 1
8
+ INITIALIZING = 2
9
+ ANALYZING = 4
10
+ PREPARING_OUTPUT = 8
11
+ ENCODING = 16
12
+ ERROR = 32
13
+
14
+ end
15
+
16
+ attr_reader :args
17
+ attr_accessor :output
18
+
19
+ event :line
20
+ event :complete
21
+ event :exited
22
+ event :status_change
23
+
24
+ @status = Status::NOT_STARTED
25
+
26
+ def self.run(args = {})
27
+ FFmpeg.new( args )
28
+ end
29
+
30
+ def initialize(params = {})
31
+ if params.class != Hash then raise ArgumentError, "Invalid parameters" end
32
+
33
+ @defaults = {
34
+ :ffmpeg => 'ffmpeg',
35
+ :options => nil
36
+ }
37
+
38
+ @params = params
39
+
40
+ @options = @params[:options]
41
+
42
+ if @options.nil? then
43
+ raise ArgumentError, "No options given"
44
+ end
45
+
46
+ @output = []
47
+
48
+ go
49
+ end
50
+
51
+ def go
52
+
53
+ @pio = IO.popen("ffmpeg #{@options} 2>&1")
54
+
55
+ temp = ''
56
+
57
+ PureMotion::Thread.new do
58
+ @pio.each_byte do |l|
59
+ if l == 10 or l == 13 then
60
+ @output.push temp
61
+ line(temp)
62
+ temp = ''
63
+ l = ''
64
+ else
65
+ l = l.chr
66
+ end
67
+ temp = temp + l
68
+ #@output << l.gsub("\r","\n")
69
+ end
70
+ if @pio.eof? then
71
+ @done = true
72
+ complete(true)
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ def ran?
79
+ @pio.eof? unless @pio.nil?
80
+ false
81
+ #!find(/^FFmpeg/).nil?
82
+ end
83
+
84
+ def ended?
85
+ return false unless ran?
86
+ @pio.eof?
87
+ end
88
+
89
+ def version
90
+ return nil unless ran?
91
+ find(/^FFmpeg version (\S+)/)
92
+ end
93
+
94
+ def find(regexp)
95
+ m = regexp.match(@output)
96
+ return m[1] if m
97
+ nil
98
+ end
99
+
100
+ def status
101
+ @status
102
+ end
103
+
104
+ private
105
+
106
+ def prepare_args
107
+ @args = []
108
+ @args << ['i', @input]
109
+
110
+ args = []
111
+
112
+ strings = []
113
+
114
+ @args.each do |arg, value|
115
+ if value.nil? then value = '' end
116
+ value = '"' + value.to_s + '"' unless value.index(' ').nil?
117
+
118
+ args << [ arg, value]
119
+
120
+ if value.empty? or value =~ /^\s*$/ then
121
+ value = ''
122
+ else
123
+ value = ' ' << value
124
+ end
125
+
126
+ strings << ( '-' << arg << value )
127
+ end
128
+
129
+ if !@output.nil? then strings << '"' + @output + '"' end
130
+
131
+ @args = strings.join(' ')
132
+ end
133
+
134
+ def status=(status_const)
135
+ @status = status_const
136
+ status_change(status_const)
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,250 @@
1
+ require 'yaml'
2
+
3
+ module PureMotion::Transcode
4
+
5
+ class Recipe
6
+
7
+ def self.from_file(file)
8
+
9
+ file.sub!(/\.yml/, '') if file =~ /\.yml/
10
+
11
+ built_in_path = File.join( File.expand_path(File.dirname(__FILE__)), '../recipes', file + '.yml')
12
+
13
+ where = nil
14
+ path = nil
15
+
16
+ if File.exists? built_in_path then where = :internal end
17
+ if File.exists? file then where = :external end
18
+
19
+ raise(ArgumentError, "Recipe file '#{file}' not found") if where.nil?
20
+
21
+ case where
22
+ when :internal
23
+ path = built_in_path
24
+ when :external
25
+ path = file
26
+ end
27
+
28
+ return self.new({ :source => :file, :recipe => path })
29
+ end
30
+
31
+ def self.from_name(name)
32
+
33
+ n = name.to_s
34
+
35
+ raise(ArgumentError, 'Invalid recipe name') if not n.is_a?(String)
36
+
37
+ return from_file(n)
38
+
39
+ end
40
+
41
+ def self.build(hash)
42
+ return self.new({ :source => :hash, :recipe => hash})
43
+ end
44
+
45
+ @recipe = {
46
+ 'format' => :skip
47
+ }
48
+ @video = {}
49
+ @audio = {}
50
+ @container = nil
51
+
52
+ attr_accessor :audio, :video, :arg_string, :args
53
+
54
+ def initialize(opts)
55
+
56
+ if not opts.is_a? Hash then raise(ArgumentError, "opts should be a hash") end
57
+
58
+ defaults = {
59
+ :source => :hash,
60
+ :recipe => nil
61
+ }
62
+
63
+ @opts = defaults.merge!(opts)
64
+
65
+ @recipe = @opts[:recipe]
66
+
67
+ if @recipe.nil? then raise(ArgumentError, "No recipe given") end
68
+
69
+ case @opts[:source]
70
+ when :hash
71
+ hash_build
72
+ when :file
73
+ file_build @recipe
74
+ when :name
75
+ from_name @recipe
76
+ else
77
+ raise(ArgumentError, 'Invalid recipe source')
78
+ end
79
+
80
+ end
81
+
82
+ def parse o
83
+
84
+ recipe = nil
85
+
86
+ recipe = o['recipe'] if o.has_key? 'recipe'
87
+ recipe = o if o.has_key? 'video' or o.has_key? 'audio'
88
+
89
+ raise ArgumentError, 'Bad recipe' if recipe.nil?
90
+
91
+ video = recipe['video']
92
+ audio = recipe['audio']
93
+
94
+ parse_video = lambda do |source_video|
95
+
96
+ @video = {}
97
+ @video.merge! source_video
98
+
99
+ # Video codec
100
+
101
+ # End video codec
102
+
103
+ # Video bitrate
104
+ if @video['bitrate'].nil? then @video['bitrate'] = :skip else
105
+ begin
106
+ @video['bitrate'] = @video['bitrate'].to_i if not @video['bitrate'].is_a? Integer
107
+ rescue
108
+ raise ArgumentError, 'Video bitrate must be a number'
109
+ end
110
+
111
+ @video['bitrate'] *= 1024
112
+ raise ArgumentError, 'Recipe: video bitrate too low' if @video['bitrate'] < 1024
113
+ end
114
+ # End video
115
+
116
+ # Video resolution
117
+ if @video['resolution'].nil? then @video['resolution'] = :skip else
118
+ if not @video['resolution'] =~ /\d*x\d*/ then
119
+ raise ArgumentError, "Invalid resolution"
120
+ end
121
+ end
122
+ # End video resolution
123
+
124
+
125
+
126
+ end
127
+
128
+ parse_audio = lambda do |source_audio|
129
+
130
+ @audio = {
131
+ 'codec' => nil,
132
+ 'bitrate' => nil,
133
+ }
134
+
135
+ @audio.merge! source_audio
136
+
137
+ # Audio codec
138
+ if @audio['codec'].nil? then @audio['codec'] = :skip else
139
+ raise ArgumentError, 'Recipe: Invalid Audio Codec' unless PureMotion::Codecs.valid? @audio['codec'], :audio
140
+ @audio['codec'] = PureMotion::Codecs.find(@audio['codec']).ffmpeg_name
141
+ end
142
+ # End Audio codec
143
+
144
+ # Audio bitrate
145
+ if @audio['bitrate'].nil? then @audio['bitrate'] = :skip else
146
+ begin
147
+ @audio['bitrate'] = @audio['bitrate'].to_i if not @audio['bitrate'].is_a? Integer
148
+ rescue
149
+ raise ArgumentError, 'Audio bitrate must be a number'
150
+ end
151
+
152
+ @audio['bitrate'] *= 1024
153
+ raise ArgumentError, 'Recipe: Audio bitrate too low' if @audio['bitrate'] < 1024
154
+ end
155
+ # End Audio Bitrate
156
+
157
+ # Audio channels
158
+ if @audio['channels'].nil? then @audio['channels'] = :skip else
159
+ channel_types = { 'mono' => 1, 'stereo' => 2 }
160
+ if @audio['channels'].is_a? String then
161
+ @audio['channels'].downcase!
162
+ if channel_types[@audio['channels']].nil? then
163
+ @audio['channels'] = :skip
164
+ else
165
+ @audio['channels'] = channel_types[@audio['channels']]
166
+ end
167
+ end
168
+ end
169
+ # End audio channels
170
+
171
+ end
172
+
173
+ parse_video.call video unless video.nil?
174
+ parse_audio.call audio unless audio.nil?
175
+
176
+ @recipe = {}
177
+
178
+ if recipe['format'].nil? then recipe['format'] = :skip else
179
+ @recipe.store('format', recipe['format'])
180
+ end
181
+
182
+ end
183
+
184
+ def valid?
185
+ valid = true
186
+ if audio? and !valid_audio? then valid = false end
187
+ if video? and !valid_video? then valid = false end
188
+ valid
189
+ end
190
+
191
+ def valid_video?
192
+
193
+ end
194
+
195
+ def valid_audio?
196
+
197
+ end
198
+
199
+ def to_args
200
+
201
+ args = []
202
+ arg_strings = []
203
+
204
+ args[0] = ['f', @recipe['format'] ]
205
+ args[1] = ['acodec', @audio['codec'] ]
206
+ args[2] = ['vcodec', @video['codec'] ]
207
+ args[3] = ['vb', @video['bitrate'] ]
208
+ args[4] = ['s', @video['resolution']]
209
+ args[5] = ['r', @video['fps'] ]
210
+ args[6] = ['ab', @audio['bitrate'] ]
211
+ args[7] = ['ac', @audio['channels'] ]
212
+
213
+ required_value_args = ['f', 'vcodec', 'acodec', 'vb', 's', 'r', 'ab', 'ac']
214
+
215
+ args.each do |arg|
216
+ next if required_value_args.include? arg[0] and arg[1].nil?
217
+ arg_strings << format_arg(arg[0], arg[1])
218
+ end
219
+
220
+ @args = arg_strings.join(' ')
221
+
222
+ @args
223
+
224
+ end
225
+
226
+ private
227
+
228
+ def format_arg name, value
229
+ name = '' if name.nil?
230
+ value = '' if value.nil?
231
+ value = value.to_s
232
+ value.gsub! '"', ''
233
+ value = '"' + value + '"' if value =~ /\s/
234
+ return value if name == :non
235
+ return name if value == :non
236
+ '-' + name.to_s + ' ' + value.to_s
237
+ end
238
+
239
+ def hash_build
240
+
241
+ end
242
+
243
+ def file_build(file)
244
+ @yaml = ::YAML.load( File.read(file) )
245
+ parse @yaml
246
+ end
247
+
248
+ end
249
+
250
+ end
@@ -0,0 +1,153 @@
1
+ module PureMotion::Transcode
2
+
3
+ @@recipes = []
4
+
5
+ def self.do(media, params)
6
+ return Transcode.new(media, params)
7
+ end
8
+
9
+ class Transcode < Object
10
+
11
+ event :progress
12
+ event :complete
13
+ event :error
14
+ event :status_change
15
+
16
+ @status
17
+ @media = nil
18
+
19
+ @ffmpeg = nil
20
+
21
+ def initialize(media, params)
22
+
23
+ @status = Status::NOT_STARTED
24
+
25
+ @params = {
26
+ :output => nil,
27
+ :overwrite => false,
28
+ :recipe => nil
29
+ }
30
+
31
+ raise ArgumentError, "Invalid paramters given" unless params.is_a? Hash
32
+
33
+ @params.merge!(params)
34
+
35
+ raise ArgumentError, "No output file given" if @params[:output].nil?
36
+
37
+ case @params[:recipe]
38
+ when String
39
+ set_recipe Recipe.from_file(@params[:recipe])
40
+ when Hash
41
+ set_recipe Recipe.build(@params[:recipe])
42
+ when Recipe
43
+ set_recipe @params[:recipe]
44
+ when Symbol
45
+ set_recipe Recipe.from_name(@params[:recipe].to_s)
46
+ else
47
+ raise ArgumentError, "Invalid recipe"
48
+ end
49
+
50
+ @media = find_media(media)
51
+
52
+ if @media.analysed?
53
+ prepare
54
+ else
55
+
56
+ @media.analysed + lambda do |media, raw|
57
+ prepare
58
+ end
59
+
60
+ end
61
+
62
+ raise ArgumentError, "Output file #{@params[:output]} exists and overwrite disabled" if File.exists?(@params[:output]) and !@params[:overwrite]
63
+
64
+ @ffmpeg = PureMotion::Tools::FFmpeg.run :options => "-i #{@media.file} #{'-y' if @params[:overwrite]} " + @recipe.to_args + " \"#{@params[:output]}\""
65
+
66
+ @ffmpeg.line + lambda do |ffmpeg, line|
67
+ handle_output line
68
+ end
69
+
70
+ end
71
+
72
+ def run
73
+
74
+ end
75
+
76
+ def cancel
77
+
78
+ end
79
+
80
+ def cancel!
81
+
82
+ end
83
+
84
+ private
85
+
86
+ def set_recipe(recipe)
87
+ @params[:recipe] = recipe
88
+ @recipe = recipe
89
+ end
90
+
91
+ def set_status(status)
92
+ @status = status
93
+ status_change(@status)
94
+ end
95
+
96
+ def find_media(media)
97
+
98
+ case media
99
+ when String
100
+ return PureMotion::Media.new(media)
101
+ when PureMotion::Media
102
+ return media
103
+ else
104
+ raise Error, "Invalid media given"
105
+ end
106
+
107
+ end
108
+
109
+ def handle_output line
110
+ puts line
111
+ # /^frame=\s*(?<frame>\d*)\s*fps=\s*(?<fps>\d*)\s*q=(?<q>\S*)\s*size=\s*(?<size>[\d]*)(?<size_unit>[kmg]B)\s*time=\s*(?<time>[\d.]*)\s*bitrate=\s*(?<bitrate>[\d.]*)(?<bitrate_unit>[km]b).*$/
112
+ progress_line = /frame=\s*(\d*)\s*fps=\s*(\d*)\s*q=(\S*)\s*[L]?size=\s*([\d]*)([kmg]B)\s*time=\s*([\d.]*)\s*bitrate=\s*([\d.]*)([km]b)/
113
+ # 1 => frame
114
+ # 2 => fps
115
+ # 3 => q
116
+ # 4 => size
117
+ # 5 => size_unit
118
+ # 6 => time
119
+ # 7 => bitrate
120
+ # 8 => bitrate_unit
121
+ handle_progress(progress_line.match(line)) if progress_line.match(line)
122
+ end
123
+
124
+ def handle_progress line
125
+ p = {
126
+ :frame => line[1].to_i,
127
+ :fps => line[2].to_i,
128
+ :q => line[3].to_f,
129
+ :size => line[4].to_i,
130
+ :time => line[6].to_f,
131
+ :bitrate => line[7].to_f,
132
+ :precent => ((100.00 / @media.detail(:duration, :seconds)) * line[6].to_f).to_i
133
+ }
134
+ p[:percent] = 100 if p[:percent] > 100
135
+ progress(p)
136
+ end
137
+
138
+ # Fire progress event
139
+ # complete(p)
140
+ end
141
+
142
+ class Error < Exception
143
+
144
+
145
+ end
146
+
147
+ class Status
148
+
149
+ NOT_STARTED = -1
150
+
151
+ end
152
+
153
+ end
data/lib/puremotion.rb ADDED
@@ -0,0 +1,22 @@
1
+ req = File.expand_path(File.dirname(__FILE__)) + '/'
2
+
3
+ require 'rubygems'
4
+
5
+ module PureMotion
6
+
7
+ end
8
+
9
+ require req + 'puremotion/events/event'
10
+ require req + 'puremotion/events/generator'
11
+
12
+ Object.extend(PureMotion::Events::Generator)
13
+
14
+ require req + 'puremotion/thread'
15
+ require req + 'puremotion/media'
16
+ require req + 'puremotion/media/stream'
17
+ require req + 'puremotion/media/stream/base'
18
+ require req + 'puremotion/media/stream/video'
19
+ require req + 'puremotion/codecs'
20
+ require req + 'puremotion/transcode/transcode'
21
+ require req + 'puremotion/transcode/recipe'
22
+ require req + 'puremotion/tools/ffmpeg'
@@ -0,0 +1,68 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{puremotion}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ominiom"]
12
+ s.date = %q{2009-11-14}
13
+ s.description = %q{A Ruby wrapper for analysis and conversion of media using FFmpeg}
14
+ s.email = %q{iain.iw.wilson@googlemail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/puremotion.rb",
27
+ "lib/puremotion/codecs.rb",
28
+ "lib/puremotion/events/event.rb",
29
+ "lib/puremotion/events/generator.rb",
30
+ "lib/puremotion/media.rb",
31
+ "lib/puremotion/media/stream.rb",
32
+ "lib/puremotion/media/stream/audio.rb",
33
+ "lib/puremotion/media/stream/base.rb",
34
+ "lib/puremotion/media/stream/collection.rb",
35
+ "lib/puremotion/media/stream/video.rb",
36
+ "lib/puremotion/recipes/ipod.yml",
37
+ "lib/puremotion/thread.rb",
38
+ "lib/puremotion/tools/ffmpeg.rb",
39
+ "lib/puremotion/transcode/recipe.rb",
40
+ "lib/puremotion/transcode/transcode.rb",
41
+ "puremotion.gemspec",
42
+ "test/helper.rb",
43
+ "test/test_puremotion.rb"
44
+ ]
45
+ s.homepage = %q{http://github.com/ominiom/puremotion}
46
+ s.rdoc_options = ["--charset=UTF-8"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = %q{1.3.5}
49
+ s.summary = %q{PureMotion}
50
+ s.test_files = [
51
+ "test/helper.rb",
52
+ "test/test_puremotion.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
60
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
61
+ else
62
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
63
+ end
64
+ else
65
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
66
+ end
67
+ end
68
+
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'puremotion'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestPuremotion < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end