vidibus-encoder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 André Pankratz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Vidibus::Encoder
2
+
3
+ This is a framework for creating custom encoders.
4
+
5
+ This gem is part of [Vidibus](http://vidibus.org), an open source toolset for building distributed (video) applications.
6
+
7
+ **Beware:** Work in progress!
8
+
9
+
10
+ ## Notes
11
+
12
+ Use FFMpeg's cropdetect to cut off black bars:
13
+ ```
14
+ ffmpeg -ss 600 -t 100 -i [input video] -vf "select='isnan(prev_selected_t)+gte(t-prev_selected_t,1)',cropdetect=24:2:0" -an -y null.mp4
15
+ ```
16
+
17
+ ## Copyright
18
+
19
+ © 2012 André Pankratz. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ $:.unshift File.expand_path('../lib/', __FILE__)
2
+
3
+ require 'bundler'
4
+ require 'rdoc/task'
5
+ require 'rspec'
6
+ require 'rspec/core/rake_task'
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ RSpec::Core::RakeTask.new(:rcov) do |t|
11
+ t.pattern = 'spec/**/*_spec.rb'
12
+ t.rcov = true
13
+ t.rcov_opts = ['--exclude', '^spec,/gems/']
14
+ end
15
+
16
+ Rake::RDocTask.new do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Vidibus::Encoder'
19
+ rdoc.rdoc_files.include('README*')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ rdoc.options << '--charset=utf-8'
22
+ end
23
+
24
+ task :default => :rcov
@@ -0,0 +1,222 @@
1
+ module Vidibus
2
+ module Encoder
3
+
4
+ # This is the main encoder that you can build your own encoders on.
5
+ #
6
+ # The workflow of a encoder is as follows:
7
+ #
8
+ # initialize
9
+ # run
10
+ # validate_options
11
+ # prepare
12
+ # => for each profile:
13
+ # preprocess
14
+ # next unless process?
15
+ # process
16
+ # postprocess
17
+ # finish
18
+ #
19
+ # Each step of the workflow is represented by a method that you may
20
+ # redefine in your custom encoder class.
21
+ class Base
22
+ extend Helper::Flags
23
+ include Helper::Base
24
+ include Helper::Tools
25
+
26
+ attr_reader :options, :tmp, :input, :output, :profile, :profiles
27
+
28
+ # Initialize a encoder instance with given options. Two options are
29
+ # mandatory:
30
+ #
31
+ # :input [String] The path to the input file
32
+ # :output [String] The path to the output file or directory
33
+ #
34
+ # You may define one or several profiles to perform. If you provide a
35
+ # hash, all profile settings required for your recipe must be included.
36
+ #
37
+ # :profile [Hash] The configuration hash of one profile
38
+ # :profiles [Hash] Hashes of several profiles with namespace
39
+ #
40
+ # Single profile example:
41
+ #
42
+ # :profile => {
43
+ # :video_bit_rate => 110000,
44
+ # :dimensions => '240x160'
45
+ # }
46
+ #
47
+ # Multi-profile example:
48
+ #
49
+ # :profiles => {
50
+ # :low => {
51
+ # :video_bit_rate => 110000,
52
+ # :dimensions => '240x160'
53
+ # },
54
+ # :high => {
55
+ # :video_bit_rate => 800000,
56
+ # :dimensions => '600x400'
57
+ # }
58
+ # }
59
+ #
60
+ # If you have registered profiles for your encoder, you may refer to
61
+ # one or several profiles by providing its name. Without a profile, the
62
+ # default one will be performed.
63
+ # To register a profile, call YourEncoder.register_profile
64
+ #
65
+ # :profile [Symbol] The name of one profile
66
+ # :profiles [Array] A list of profile names
67
+ #
68
+ # @options [Hash] The configuration options
69
+ def initialize(options = {})
70
+ @options = options
71
+ [:tmp, :input, :output, :profile, :profiles].each do |attribute|
72
+ self.send("#{attribute}=", options[attribute]) if options[attribute]
73
+ end
74
+ set_default_options
75
+ end
76
+
77
+ # Perform the encoding workflow.
78
+ # All profiles will be performed in order. Lowest bit_rate first.
79
+ def run
80
+ validate_options
81
+ prepare
82
+ profiles.sorted.each do |@profile|
83
+ next unless process?
84
+ preprocess
85
+ process
86
+ postprocess
87
+ end
88
+ finish
89
+ end
90
+
91
+ # Fixed profile presets for this encoder.
92
+ # You may define profile presets inside your encoder class.
93
+ #
94
+ # When defining settings, you should define a :default setting as well
95
+ # to support single profile encodings.
96
+ # An example:
97
+ #
98
+ # {
99
+ # :low => {
100
+ # :video_bit_rate => 110000,
101
+ # :dimensions => '240x160'
102
+ # },
103
+ # :high => {
104
+ # :video_bit_rate => 800000,
105
+ # :dimensions => '600x400'
106
+ # }
107
+ # }.tap do |p|
108
+ # p[:default] = p[:high]
109
+ # end
110
+ def self.profile_presets; end
111
+
112
+ # Define a default file extension for your encoder.
113
+ # Or define one in a profile configuration.
114
+ def self.file_extension; end
115
+
116
+ class << self
117
+ attr_accessor :registered_profiles
118
+ @registered_profiles = {}
119
+
120
+ # Register a profile with given name and settings.
121
+ def register_profile(name, settings)
122
+ @registered_profiles[name] = settings
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ attr_reader :flags
129
+
130
+ # This method holds the recipe to perform.
131
+ # It is required that you define a custom encoding recipe inside your
132
+ # encoder class.
133
+ #
134
+ # The recipe must be a executable string that may contain placeholders
135
+ # that we call 'flags'. A simplified example:
136
+ #
137
+ # 'ffmpeg -i %{input} -threads 0 -y %{output}'
138
+ #
139
+ def recipe
140
+ raise(RecipeError, 'Please define an encoding recipe inside your encoder class')
141
+ end
142
+
143
+ # Handle the response returned from processing.
144
+ # Define a response handler for your recipe inside your encoder class.
145
+ #
146
+ # TODO: Example
147
+ def handle_response(stdout, stderr); end
148
+
149
+ # Set some default options.
150
+ def set_default_options
151
+ @profiles ||= Util::Profiles.new(:base => self)
152
+ @tmp ||= Util::Tmp.new(:base => self)
153
+ @flags ||= Util::Flags.new(:base => self)
154
+ end
155
+
156
+ # Ensure that valid options are given.
157
+ # Please override this method if you need checks for custom arguments.
158
+ #
159
+ # By default, input, output, and profiles will be checked.
160
+ def validate_options
161
+ input ? input.validate : raise(InputError, 'No input defined')
162
+ output ? output.validate : raise(OutputError, 'No output defined')
163
+ profiles ? profiles.validate : raise(ProfileError, 'No profiles defined')
164
+ flags ? flags.validate : raise(FlagError, 'No flags defined')
165
+ end
166
+
167
+ # Prepare for encoding.
168
+ # Please override this method inside your encoder
169
+ # class, if you need custom preparation.
170
+ #
171
+ # Currently, the tmp folder will be created.
172
+ def prepare
173
+ tmp.make_dir
174
+ end
175
+
176
+ # Decide whether the current profile should be processed.
177
+ def process?
178
+ true
179
+ end
180
+
181
+ # Preprocess each encoding profile.
182
+ # Downsize video bit rate if it exceeds input bit rate.
183
+ def preprocess
184
+ if higher_bit_rate?
185
+ profile.settings[:video_bit_rate] = input.bit_rate
186
+ end
187
+ end
188
+
189
+ # Return true if wanted bit rate is higher than input's one.
190
+ # Allow 5% tolerance.
191
+ def higher_bit_rate?
192
+ input.bit_rate && profile.bit_rate * 1.05 > input.bit_rate
193
+ end
194
+
195
+ # Perform the encoding command.
196
+ # TODO: Describe.
197
+ def process
198
+ cmd = flags.render(recipe)
199
+ logger.info("\nEncoding profile #{profile.name}...\n#{cmd}\n")
200
+ pid, stdin, stdout, stderr = POSIX::Spawn::popen4(cmd)
201
+ handle_response(stdout, stderr)
202
+ ppid, status = Process::wait2(pid)
203
+ unless status.exitstatus == 0
204
+ raise(ProcessingError, "Execution failed:\n#{stderr.read}")
205
+ end
206
+ ensure # close all streams
207
+ [stdin, stdout, stderr].each { |io| io.close rescue nil }
208
+ end
209
+
210
+ # Postprocess each encoding profile.
211
+ def postprocess; end
212
+
213
+ # Hook for finishing touches.
214
+ # TODO: Describe.
215
+ def finish
216
+ encoded_files = output.copy_files
217
+ tmp.remove_dir
218
+ encoded_files
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,47 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Helper
4
+
5
+ # This helper provides methods for the encoder base class.
6
+ module Base
7
+
8
+ # Define setters for profile options.
9
+ # For either option, :profile or :profiles, a Util::Profiles object
10
+ # will be initialized.
11
+ [:profile, :profiles].each do |option|
12
+ class_eval <<-EOT
13
+ def #{option}=(settings)
14
+ raise(ArgumentError, "Nil is not allowed") unless settings
15
+ @profiles = Util::Profiles.new(
16
+ #{option.inspect} => settings, :base => self
17
+ )
18
+ end
19
+ EOT
20
+ end
21
+
22
+ # Define setters for other options.
23
+ # For each option a Util:: Object will be initialized, which are
24
+ # Util::Tmp, Util::Input, and Util::Output.
25
+ [:tmp, :input, :output].each do |option|
26
+ class_eval <<-EOT
27
+ def #{option}=(input)
28
+ raise(ArgumentError, "Nil is not allowed") unless input
29
+ util = Util::#{option.to_s.classify}
30
+ @#{option} = input.is_a?(util) ? input : util.new(:path => input, :base => self )
31
+ end
32
+ EOT
33
+ end
34
+
35
+ # TODO: DESCRIBE
36
+ def uuid
37
+ @uuid ||= Vidibus::Uuid.generate
38
+ end
39
+
40
+ # Reader for the logger.
41
+ def logger
42
+ Vidibus::Encoder.logger
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,35 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Helper
4
+ module Flags
5
+
6
+ # Register the inheritable reader +registered_flags+ on class level.
7
+ def self.extended(base)
8
+ base.class_eval <<-RUBY
9
+ unless defined?(@@registered_flags)
10
+ @@registered_flags = {}
11
+ end
12
+
13
+ def self.registered_flags
14
+ @@registered_flags
15
+ end
16
+ RUBY
17
+ end
18
+
19
+ # Register a flag handler. A flag handler will be called when
20
+ # rendering the encoding recipe if a matching profile setting is
21
+ # available.
22
+ #
23
+ # Usage:
24
+ #
25
+ # class MyEncoder < Vidibus::Encoder::Base
26
+ # flag(:active) { |value| "-v #{value}"}
27
+ # end
28
+ def flag(name, &block)
29
+ raise(ArgumentError, 'Block is missing') unless block_given?
30
+ registered_flags[name.to_sym] = block
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Helper
4
+ module Tools
5
+
6
+ # Return a matching frame rate from given list.
7
+ # You may use this method to determine which of your valid frame rates
8
+ # fits the input best.
9
+ def matching_frame_rate(list)
10
+ raise(ArgumentError, 'Argument must be an array') unless list && list.is_a?(Array)
11
+ input_frame_rate = input.frame_rate
12
+ list.each do |rate|
13
+ return rate if rate == input_frame_rate
14
+ end
15
+ # Detect the smallest multiple of any list entry
16
+ lowest_q = nil
17
+ wanted = nil
18
+ list.each do |rate|
19
+ q, r = input_frame_rate.divmod(rate)
20
+ if r == 0 && (!lowest_q || lowest_q > q)
21
+ lowest_q = q
22
+ wanted = rate
23
+ end
24
+ end
25
+ wanted
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ require 'vidibus/encoder/helper/base'
2
+ require 'vidibus/encoder/helper/flags'
3
+ require 'vidibus/encoder/helper/tools'
@@ -0,0 +1,88 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Flags
5
+ include Enumerable
6
+
7
+ attr_accessor :base
8
+
9
+ # Initialize a flags object.
10
+ # One option is required to pass validation:
11
+ #
12
+ # :base [Vidibus::Encoder::Base] The encoder object
13
+ def initialize(options = {})
14
+ @base = options[:base]
15
+ end
16
+
17
+ # Ensure that the base attribute is around.
18
+ def validate
19
+ raise(FlagError, 'Define a base class for flags') unless base
20
+ end
21
+
22
+ # This method turns the recipe into a command string by replacing all
23
+ # placeholders and removing empty ones.
24
+ #
25
+ # If a flag handler is defined for a placeholder and the profile
26
+ # setting is present, the flag handler will be called.
27
+ #
28
+ # Examples:
29
+ #
30
+ # base = Vidibus::Encoder::Base.new
31
+ # flags = Vidibus::Encoder::Util::Flags.new(:base => base)
32
+ # profile = Vidibus::Encoder::Util::Profile.new(:base => base)
33
+ # encoder.instance_variable_set('@profile', profile)
34
+ #
35
+ # recipe = 'some %{thing}'
36
+ #
37
+ # # Without a matching profile setting
38
+ # flags.render(recipe)
39
+ # # => 'some '
40
+ #
41
+ # # With a matching profile setting
42
+ # encoder.profile.settings[:thing] = 'beer'
43
+ # flags.render(recipe)
44
+ # # => 'some beer'
45
+ #
46
+ # # With a matching profile setting and flag handler
47
+ # encoder.profile.settings[:thing] = 'beer'
48
+ # encoder.class.flag(:thing) { |value| "cold #{value}" }
49
+ # flags.render(recipe)
50
+ # # => 'some cold beer'
51
+ def render(recipe)
52
+ recipe = recipe.gsub(/%\{([^\{]+)\}/) do |match|
53
+ flag = $1.to_sym
54
+ value = base.profile.try!(flag)
55
+ if value
56
+ if handler = base.class.registered_flags[flag]
57
+ match = base.instance_exec(value, &handler)
58
+ else
59
+ match = value
60
+ end
61
+ end
62
+ match
63
+ end
64
+ recipe = render_input(recipe)
65
+ recipe = render_output(recipe)
66
+ cleanup(recipe)
67
+ end
68
+
69
+ # Replace %{input} placeholder in recipe.
70
+ def render_input(recipe)
71
+ recipe % {:input => %("#{base.input}")}
72
+ end
73
+
74
+ # Replace %{output} placeholder in recipe.
75
+ def render_output(recipe)
76
+ return recipe unless base.input && base.output
77
+ output = base.tmp.join(base.output.file_name)
78
+ recipe % {:output => %("#{output}")}
79
+ end
80
+
81
+ # Remove empty placeholders.
82
+ def cleanup(recipe)
83
+ recipe.gsub(/%\{[^\{]+\}/, '').gsub(/ +/, ' ')
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,63 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Input
5
+
6
+ attr_accessor :path
7
+ attr_reader :properties
8
+
9
+ # Initialize an input object.
10
+ # One option is required:
11
+ #
12
+ # :path [String] The path to the input file
13
+ def initialize(options)
14
+ @path = options[:path]
15
+ set_properties!
16
+ end
17
+
18
+ # Return the path.
19
+ def to_s
20
+ path
21
+ end
22
+
23
+ # Return true if path is readable.
24
+ def readable?
25
+ File.readable?(path)
26
+ end
27
+
28
+ # Ensure that a path is given and readable.
29
+ def validate
30
+ readable? || raise(InputError, 'Input is not readable')
31
+ end
32
+
33
+ # Return aspect ratio of input file.
34
+ def aspect
35
+ @aspect ||= width/height.to_f
36
+ end
37
+
38
+ private
39
+
40
+ # Analyze file info of input and set properties.
41
+ # If analysis fails, a DataError will be raised.
42
+ def set_properties!
43
+ return unless present?
44
+ begin
45
+ @properties = Fileinfo(path)
46
+ rescue => error
47
+ end
48
+ @properties || raise(DataError, "Extracting input data failed!\n#{error}\n")
49
+ end
50
+
51
+ # Try to return value from properties hash. Return nil if property is
52
+ # undefined or nil.
53
+ def method_missing(sym, *arguments)
54
+ if properties && value = properties[sym]
55
+ value
56
+ else
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,98 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Output
5
+
6
+ attr_accessor :path
7
+ attr_reader :base
8
+
9
+ # Initialize an output object.
10
+ # Two options are required:
11
+ #
12
+ # :base [Vidibus::Encoder::Base] The encoder object
13
+ # :path [String] The path to the output file or directory
14
+ def initialize(options)
15
+ @base = options[:base]
16
+ @path = options[:path]
17
+ make_dir
18
+ end
19
+
20
+ # Return the output path.
21
+ def to_s
22
+ file_path || path
23
+ end
24
+
25
+ # Return the directory name from path.
26
+ def dir
27
+ @dir ||= (!exist? || directory?) ? path : File.dirname(path)
28
+ end
29
+
30
+ # Extract the file name from given path or input file.
31
+ def file_name
32
+ path[/([^\/]+\.[^\/]+)$/, 1] || begin
33
+ if base.input
34
+ base_name(base.input.path).tap do |name|
35
+ if base.profile
36
+ name << ".#{base.profile.file_extension}"
37
+ if base.profile.name && base.profile.name.to_s != 'default'
38
+ name.gsub!(/(\.[^\.]+)$/, "-#{base.profile.name}\\1")
39
+ end
40
+ else
41
+ raise(OutputError, 'Could not determine file name because the current profile does not define a file extension')
42
+ end
43
+ end
44
+ else
45
+ raise(OutputError, 'Could not determine file name from input or output path')
46
+ end
47
+ end
48
+ end
49
+
50
+ def file_path
51
+ File.join(dir, file_name) if file_name
52
+ end
53
+
54
+ def base_name(str = file_name)
55
+ str[/([^\/]+)\.[^\.]+$/, 1]
56
+ end
57
+
58
+ # Return true if a path has been defined.
59
+ def present?
60
+ !!path
61
+ end
62
+
63
+ # Return true if path exists
64
+ def exist?
65
+ File.exist?(path)
66
+ end
67
+
68
+ # Return true if path is a directory
69
+ def directory?
70
+ File.directory?(path)
71
+ end
72
+
73
+ # Ensure that a path is given.
74
+ def validate
75
+ present? || raise(OutputError, 'No output defined')
76
+ end
77
+
78
+ # Create output directory
79
+ def make_dir
80
+ FileUtils.mkdir_p(dir) unless exist?
81
+ end
82
+
83
+ # Copy files from tmp folder to output folder.
84
+ def copy_files
85
+ begin
86
+ files = Dir.glob("#{base.tmp}/*")
87
+ FileUtils.cp_r(files, dir)
88
+ files.each do |file|
89
+ file.gsub!(base.tmp.to_s, dir)
90
+ end
91
+ rescue => e
92
+ raise("Copying output files from #{base.tmp} to #{path} failed: #{e.message}")
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,173 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Profile
5
+ attr_accessor :name, :settings, :base
6
+
7
+ def initialize(options = {})
8
+ @name = options[:name]
9
+ @settings = options[:settings] || {}
10
+ @base = options[:base]
11
+ end
12
+
13
+ # Sum up audio and video bit_rate unless bit_rate
14
+ # has been defined in settings.
15
+ def bit_rate
16
+ settings[:bit_rate] || audio_bit_rate.to_i + video_bit_rate.to_i
17
+ end
18
+
19
+ # Ensure that all required attribtues have been set.
20
+ def validate
21
+ raise(ProfileError, 'Define a name for this profile') if [nil, ''].include?(name)
22
+ raise(ProfileError, 'Define a settings hash for this profile') unless settings.is_a?(Hash) && settings.any?
23
+ raise(ProfileError, 'Define a encoder class for this profile') unless base
24
+ end
25
+
26
+ # Return a list of all profile attributes
27
+ # including the given settings.
28
+ def attributes
29
+ @attributes ||= (settings.keys.map(&:to_s) + %w[width height dimensions]).sort.uniq
30
+ end
31
+
32
+ # Return the width. If the wanted width exceeds the
33
+ # input's one, it will be scaled down.
34
+ #
35
+ # Define a modulus attribute to adjust dimensions. For best
36
+ # encoding results, the modulus should be 16. 8, 4, and
37
+ # even 2 will also work, but image quality increases with
38
+ # higher numbers because compression works better.
39
+ #
40
+ # @modulus [Integer] The modulus for rounding the value
41
+ #
42
+ # @return [Integer] The width
43
+ def width(modulus = 1)
44
+ @width ||= {}
45
+ @width[modulus] = dim(:width, modulus)
46
+ end
47
+
48
+ # Return the height. If the wanted height exceeds the
49
+ # input's one, it will be scaled down.
50
+ #
51
+ # Define a modulus attribute to adjust dimensions. For best
52
+ # encoding results, the modulus should be 16. 8, 4, and
53
+ # even 2 will also work, but image quality increases with
54
+ # higher numbers because compression works better.
55
+ #
56
+ # @modulus [Integer] The modulus for rounding the value
57
+ #
58
+ # @return [Integer] The height
59
+ def height(modulus = 1)
60
+ @height ||= {}
61
+ @height[modulus] = dim(:height, modulus)
62
+ end
63
+
64
+ # Return dimensions. If wanted dimensions exceed the input's
65
+ # ones, they will be scaled down.
66
+ #
67
+ # Define a modulus attribute to adjust dimensions. For best
68
+ # encoding results, the modulus should be 16. 8, 4, and
69
+ # even 2 will also work, but image quality increases with
70
+ # higher numbers because compression works better.
71
+ #
72
+ # @modulus [Integer] The modulus for rounding the value
73
+ #
74
+ # @return [String] The dimensions
75
+ def dimensions(modulus = 1)
76
+ @dimensions ||= {}
77
+ @dimensions[modulus] = begin
78
+ "#{width(modulus)}x#{height(modulus)}"
79
+ end
80
+ end
81
+
82
+ # Return the aspect ratio of width to height as string like "16:9".
83
+ # Define a modulus attribute to adjust dimensions.
84
+ #
85
+ # @modulus [Integer] The modulus for rounding the value
86
+ #
87
+ # @return [String] The dimensions
88
+ def aspect_ratio(modulus = 1)
89
+ @aspect_ratio ||= settings[:aspect_ratio] ||= begin
90
+ w = width(modulus)
91
+ h = height(modulus)
92
+ if w > 0 && h > 0
93
+ w/h.to_f
94
+ else
95
+ 1
96
+ end
97
+ end
98
+ end
99
+
100
+ def file_extension
101
+ @file_extension ||= settings[:file_extension] || base.class.file_extension || raise(ProfileError, 'Define a file extension for this profile')
102
+ end
103
+
104
+ private
105
+
106
+ # Try to return value from settings hash. Return nil if setting is
107
+ # undefined or nil.
108
+ def method_missing(sym, *arguments)
109
+ if settings && value = settings[sym]
110
+ value
111
+ # elseif TODO: check if it's a setter
112
+ else
113
+ nil
114
+ end
115
+ end
116
+
117
+ # Return the wanted dimension. If it exceeds the input's
118
+ # one, it will be scaled down.
119
+ #
120
+ # The dimension will be optained from one of
121
+ # the following sources:
122
+ # 1. from the value given in settings
123
+ # 2. from dimensions given in settings
124
+ # 3. calculated from opposite value, if given
125
+ # 4. from the input's properties
126
+ #
127
+ # @wanted [Symbol] The wanted dimension: :width or :height
128
+ # @modulus [Integer] The modulus for rounding the value
129
+ #
130
+ # @return [Integer] The width
131
+ def dim(wanted, modulus = 1)
132
+ modulus = modulus.to_i
133
+
134
+ w = (wanted == :width)
135
+ value = settings[wanted]
136
+ given = base.input.send(wanted).to_f
137
+
138
+ opposite = w ? :height : :width
139
+ _value = settings[opposite]
140
+ _given = base.input.send(opposite).to_f
141
+
142
+ if !value && settings[:dimensions]
143
+ matches = settings[:dimensions].match(/(\d+)x(\d+)/).to_a
144
+ i = w ? 1 : -1
145
+ value = matches[i]
146
+ _value ||= matches[-i]
147
+ end
148
+
149
+ if value
150
+ value = value.to_i
151
+ if value > given
152
+ value = given
153
+ end
154
+ if _value && _value.to_i > _given
155
+ value *= _given/_value.to_f
156
+ end
157
+ else
158
+ value = given
159
+ if _value && _value.to_i < _given
160
+ value *= _value.to_f/_given
161
+ end
162
+ end
163
+
164
+ if modulus > 1
165
+ q, r = value.to_i.divmod(modulus)
166
+ value = q * modulus
167
+ end
168
+ value.round
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,119 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Profiles
5
+ include Enumerable
6
+
7
+ attr_reader :profile, :profiles, :base
8
+
9
+ def initialize(options)
10
+ @base = options[:base]
11
+ @profile = options[:profile]
12
+ @profiles = options[:profiles]
13
+ end
14
+
15
+ # Return all profiles available for encoder base.
16
+ # For better encapsulation this method is placed here.
17
+ def available
18
+ @available ||= begin
19
+ (base.class.registered_profiles || {}).tap do |items|
20
+ items.merge!(base.class.profile_presets) if base.class.profile_presets
21
+ end
22
+ end
23
+ end
24
+
25
+ # Return the used profile(s). If no profile is used, an empty hash
26
+ # will be returned.
27
+ #
28
+ # @return [Hash] A collection of profile objects
29
+ def collection
30
+ @collection ||= begin
31
+ begin
32
+ map
33
+ rescue ProfileError
34
+ {}
35
+ end
36
+ end
37
+ end
38
+ alias :to_h :collection
39
+
40
+ # Iterate over the used profiles.
41
+ def each
42
+ collection.each do |profile|
43
+ yield(profile)
44
+ end
45
+ end
46
+
47
+ # Return the used profiles, sorted by given attribute.
48
+ #
49
+ # attribute [Hash] A collection of profile objects
50
+ #
51
+ # Default sorting attribute is :bit_rate.
52
+ def sorted(attribute = :bit_rate)
53
+ @sorted ||= {}
54
+ @sorted[attribute] ||= sort_by { |p| p.send(attribute) }
55
+ end
56
+
57
+ # Return true if profile config is available, raise a ProfileError
58
+ # otherwise.
59
+ def validate
60
+ !!map || raise(ProfileError, 'No profiles defined')
61
+ end
62
+
63
+ # Return true if several profiles are in use.
64
+ def multi?
65
+ @is_multi ||= used.count > 1
66
+ end
67
+
68
+ private
69
+
70
+ # Return a collection of mapped profile objects.
71
+ def map
72
+ @map ||= config.map do |name, settings|
73
+ Profile.new.tap do |profile|
74
+ profile.name = name
75
+ profile.settings = settings
76
+ profile.base = base
77
+ profile.validate
78
+ end
79
+ end
80
+ end
81
+
82
+ # Return a profile hash for any given profile.
83
+ def config
84
+ @config ||= multi_config || single_config
85
+ end
86
+
87
+ # Return a config hash for wanted profiles.
88
+ def multi_config
89
+ if profiles.is_a?(Hash)
90
+ profiles
91
+ elsif profiles.is_a?(Array)
92
+ {}.tap do |p|
93
+ for name in profiles
94
+ p[name] = available[name] ||
95
+ raise(ProfileError, "Profile #{name.inspect} is undefined")
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Return a config hash for wanted profile.
102
+ def single_config
103
+ default = begin
104
+ if profile.is_a?(Hash)
105
+ profile
106
+ elsif [String, Symbol].include?(profile.class)
107
+ available[profile] ||
108
+ raise(ProfileError, "Profile #{profile.inspect} is undefined")
109
+ else
110
+ available[:default] ||
111
+ raise(ProfileError, 'No default profile defined')
112
+ end
113
+ end
114
+ {:default => default}
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,44 @@
1
+ module Vidibus
2
+ module Encoder
3
+ module Util
4
+ class Tmp
5
+ DEFAULT = '/tmp/vidibus-encoder'
6
+
7
+ attr_reader :path, :base
8
+
9
+ # Initialize a tmp folder object.
10
+ # One option is required:
11
+ #
12
+ # :base [Vidibus::Encoder::Base] The encoder object
13
+ #
14
+ # One option is optional:
15
+ #
16
+ # :path [String] The path to the tmp folder
17
+ def initialize(options)
18
+ @base = options[:base]
19
+ @path = File.join(options[:path] || DEFAULT, base.uuid)
20
+ end
21
+
22
+ # Return the default path.
23
+ def to_s
24
+ path
25
+ end
26
+
27
+ # Return a path with additional arguments.
28
+ def join(*args)
29
+ File.join(path, *args)
30
+ end
31
+
32
+ # Make a temporary folder.
33
+ def make_dir
34
+ FileUtils.mkdir_p(path)
35
+ end
36
+
37
+ # Remove the temporary folder.
38
+ def remove_dir
39
+ FileUtils.remove_dir(path) if File.exist?(path) && path.length > 3
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ require 'vidibus/encoder/util/tmp'
2
+ require 'vidibus/encoder/util/input'
3
+ require 'vidibus/encoder/util/output'
4
+ require 'vidibus/encoder/util/profile'
5
+ require 'vidibus/encoder/util/profiles'
6
+ require 'vidibus/encoder/util/flags'
@@ -0,0 +1,5 @@
1
+ module Vidibus
2
+ module Encoder
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ require 'vidibus/encoder/helper'
2
+ require 'vidibus/encoder/util'
3
+ require 'vidibus/encoder/base'
4
+
5
+ module Vidibus
6
+ module Encoder
7
+ extend self
8
+
9
+ class Error < StandardError; end
10
+ class ProcessingError < Error; end
11
+ class DataError < Error; end
12
+ class ConfigurationError < Error; end
13
+ class InputError < ConfigurationError; end
14
+ class OutputError < ConfigurationError; end
15
+ class ProfileError < ConfigurationError; end
16
+ class RecipeError < ConfigurationError; end
17
+ class FlagError < ConfigurationError; end
18
+
19
+ attr_accessor :formats
20
+ @formats = {}
21
+
22
+ # Register a new encoder format.
23
+ def register_format(name, processor)
24
+ unless processor.new.is_a?(Vidibus::Encoder::Base)
25
+ raise(ArgumentError, 'The processor must inherit Vidibus::Encoder::Base')
26
+ end
27
+ @formats ||= {}
28
+ @formats[name] = processor
29
+ end
30
+
31
+ # Return the custom or standard logger.
32
+ # If Rails is around, Rails.logger will be used
33
+ # by default.
34
+ def logger
35
+ @logger ||= begin
36
+ defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
37
+ end
38
+ end
39
+
40
+ # Set a custom logger instance.
41
+ def logger=(instance)
42
+ @logger = instance
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ require 'posix/spawn'
2
+ require 'vidibus-fileinfo'
3
+ require 'vidibus-uuid'
4
+
5
+ require 'vidibus/encoder'
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vidibus-encoder
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - "Andr\xC3\xA9 Pankratz"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-04-20 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: posix-spawn
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: vidibus-fileinfo
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 23
44
+ segments:
45
+ - 1
46
+ - 0
47
+ - 0
48
+ version: 1.0.0
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: vidibus-uuid
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ type: :runtime
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: bundler
67
+ prerelease: false
68
+ requirement: &id004 !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 23
74
+ segments:
75
+ - 1
76
+ - 0
77
+ - 0
78
+ version: 1.0.0
79
+ type: :development
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ name: rake
83
+ prerelease: false
84
+ requirement: &id005 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ type: :development
94
+ version_requirements: *id005
95
+ - !ruby/object:Gem::Dependency
96
+ name: rdoc
97
+ prerelease: false
98
+ requirement: &id006 !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ type: :development
108
+ version_requirements: *id006
109
+ - !ruby/object:Gem::Dependency
110
+ name: rcov
111
+ prerelease: false
112
+ requirement: &id007 !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ type: :development
122
+ version_requirements: *id007
123
+ - !ruby/object:Gem::Dependency
124
+ name: rspec
125
+ prerelease: false
126
+ requirement: &id008 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ hash: 7
132
+ segments:
133
+ - 2
134
+ version: "2"
135
+ type: :development
136
+ version_requirements: *id008
137
+ - !ruby/object:Gem::Dependency
138
+ name: rr
139
+ prerelease: false
140
+ requirement: &id009 !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ hash: 3
146
+ segments:
147
+ - 0
148
+ version: "0"
149
+ type: :development
150
+ version_requirements: *id009
151
+ description: Encoder framework
152
+ email: andre@vidibus.com
153
+ executables: []
154
+
155
+ extensions: []
156
+
157
+ extra_rdoc_files: []
158
+
159
+ files:
160
+ - lib/vidibus/encoder/base.rb
161
+ - lib/vidibus/encoder/helper/base.rb
162
+ - lib/vidibus/encoder/helper/flags.rb
163
+ - lib/vidibus/encoder/helper/tools.rb
164
+ - lib/vidibus/encoder/helper.rb
165
+ - lib/vidibus/encoder/util/flags.rb
166
+ - lib/vidibus/encoder/util/input.rb
167
+ - lib/vidibus/encoder/util/output.rb
168
+ - lib/vidibus/encoder/util/profile.rb
169
+ - lib/vidibus/encoder/util/profiles.rb
170
+ - lib/vidibus/encoder/util/tmp.rb
171
+ - lib/vidibus/encoder/util.rb
172
+ - lib/vidibus/encoder/version.rb
173
+ - lib/vidibus/encoder.rb
174
+ - lib/vidibus-encoder.rb
175
+ - LICENSE
176
+ - README.md
177
+ - Rakefile
178
+ has_rdoc: true
179
+ homepage: https://github.com/vidibus/vidibus-encoder
180
+ licenses: []
181
+
182
+ post_install_message:
183
+ rdoc_options: []
184
+
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ none: false
189
+ requirements:
190
+ - - ">="
191
+ - !ruby/object:Gem::Version
192
+ hash: 3
193
+ segments:
194
+ - 0
195
+ version: "0"
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ none: false
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ hash: 23
202
+ segments:
203
+ - 1
204
+ - 3
205
+ - 6
206
+ version: 1.3.6
207
+ requirements: []
208
+
209
+ rubyforge_project: vidibus-encoder
210
+ rubygems_version: 1.3.7
211
+ signing_key:
212
+ specification_version: 3
213
+ summary: Encoder framework
214
+ test_files: []
215
+