vidibus-encoder 0.1.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/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
+