apple-tv-converter 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/Gemfile +11 -0
  2. data/Gemfile.lock +44 -0
  3. data/README.md +3 -2
  4. data/bin/SublerCLI +0 -0
  5. data/gems/streamio-ffmpeg/CHANGELOG +198 -0
  6. data/gems/streamio-ffmpeg/Gemfile +3 -0
  7. data/gems/streamio-ffmpeg/LICENSE +20 -0
  8. data/gems/streamio-ffmpeg/README.md +180 -0
  9. data/gems/streamio-ffmpeg/Rakefile +22 -0
  10. data/gems/streamio-ffmpeg/lib/ffmpeg/encoding_options.rb +139 -0
  11. data/gems/streamio-ffmpeg/lib/ffmpeg/errors.rb +4 -0
  12. data/gems/streamio-ffmpeg/lib/ffmpeg/io_monkey.rb +42 -0
  13. data/gems/streamio-ffmpeg/lib/ffmpeg/movie.rb +206 -0
  14. data/gems/streamio-ffmpeg/lib/ffmpeg/stream.rb +15 -0
  15. data/gems/streamio-ffmpeg/lib/ffmpeg/transcoder.rb +120 -0
  16. data/gems/streamio-ffmpeg/lib/ffmpeg/version.rb +3 -0
  17. data/gems/streamio-ffmpeg/lib/streamio-ffmpeg.rb +49 -0
  18. data/gems/streamio-ffmpeg/spec/ffmpeg/encoding_options_spec.rb +135 -0
  19. data/gems/streamio-ffmpeg/spec/ffmpeg/movie_spec.rb +325 -0
  20. data/gems/streamio-ffmpeg/spec/ffmpeg/transcoder_spec.rb +210 -0
  21. data/gems/streamio-ffmpeg/spec/fixtures/bin/ffmpeg-audio-only +62 -0
  22. data/gems/streamio-ffmpeg/spec/fixtures/bin/ffmpeg-hanging +44 -0
  23. data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome movie.mov +0 -0
  24. data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome'movie.mov +0 -0
  25. data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome_widescreen.mov +0 -0
  26. data/gems/streamio-ffmpeg/spec/fixtures/movies/broken.mp4 +0 -0
  27. data/gems/streamio-ffmpeg/spec/fixtures/movies/empty.flv +0 -0
  28. data/gems/streamio-ffmpeg/spec/fixtures/movies/sideways movie.mov +0 -0
  29. data/gems/streamio-ffmpeg/spec/fixtures/movies/weird_aspect.small.mpg +0 -0
  30. data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_iso-8859-1.txt +35 -0
  31. data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_no_audio.txt +18 -0
  32. data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_non_supported_audio.txt +29 -0
  33. data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_surround_sound.txt +19 -0
  34. data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_weird_dar.txt +24 -0
  35. data/gems/streamio-ffmpeg/spec/fixtures/sounds/napoleon.mp3 +0 -0
  36. data/gems/streamio-ffmpeg/spec/spec_helper.rb +21 -0
  37. data/gems/streamio-ffmpeg/spec/streamio-ffmpeg_spec.rb +40 -0
  38. data/gems/streamio-ffmpeg/streamio-ffmpeg.gemspec +19 -0
  39. data/lib/apple_tv_converter.rb +10 -4
  40. data/lib/apple_tv_converter/apple_tv_converter.sublime-project +0 -6
  41. data/lib/apple_tv_converter/apple_tv_converter.sublime-workspace +501 -378
  42. data/lib/apple_tv_converter/command_line.rb +46 -22
  43. data/lib/apple_tv_converter/media.rb +49 -16
  44. data/lib/apple_tv_converter/media_converter.rb +19 -18
  45. data/lib/apple_tv_converter/media_converter_adapter.rb +58 -14
  46. data/lib/apple_tv_converter/media_converter_mac_adapter.rb +46 -35
  47. data/lib/apple_tv_converter/version.rb +1 -1
  48. metadata +49 -12
@@ -0,0 +1,22 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new('spec') do |t|
7
+ t.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
11
+
12
+ desc "Push a new version to Rubygems"
13
+ task :publish do
14
+ require 'ffmpeg/version'
15
+
16
+ sh "gem build streamio-ffmpeg.gemspec"
17
+ sh "gem push streamio-ffmpeg-#{FFMPEG::VERSION}.gem"
18
+ sh "git tag v#{FFMPEG::VERSION}"
19
+ sh "git push origin v#{FFMPEG::VERSION}"
20
+ sh "git push origin master"
21
+ sh "rm streamio-ffmpeg-#{FFMPEG::VERSION}.gem"
22
+ end
@@ -0,0 +1,139 @@
1
+ module FFMPEG
2
+ class EncodingOptions < Hash
3
+ def initialize(options = {})
4
+ merge!(options)
5
+ end
6
+
7
+ def to_s
8
+ params = collect do |key, value|
9
+ send("convert_#{key}", value) if value && supports_option?(key)
10
+ end
11
+
12
+ # codecs should go before the presets so that the files will be matched successfully
13
+ # all other parameters go after so that we can override whatever is in the preset
14
+ codecs = params.select { |p| p =~ /codec/ }
15
+ presets = params.select { |p| p =~ /\-.pre/ }
16
+ other = params - codecs - presets
17
+ params = codecs + presets + other
18
+
19
+ params_string = params.join(" ")
20
+ params_string << " #{convert_aspect(calculate_aspect)}" if calculate_aspect?
21
+ params_string
22
+ end
23
+
24
+ def width
25
+ self[:resolution].split("x").first.to_i rescue nil
26
+ end
27
+
28
+ def height
29
+ self[:resolution].split("x").last.to_i rescue nil
30
+ end
31
+
32
+ private
33
+ def supports_option?(option)
34
+ option = RUBY_VERSION < "1.9" ? "convert_#{option}" : "convert_#{option}".to_sym
35
+ private_methods.include?(option)
36
+ end
37
+
38
+ def convert_aspect(value)
39
+ "-aspect #{value}"
40
+ end
41
+
42
+ def calculate_aspect
43
+ width, height = self[:resolution].split("x")
44
+ width.to_f / height.to_f
45
+ end
46
+
47
+ def calculate_aspect?
48
+ self[:aspect].nil? && self[:resolution]
49
+ end
50
+
51
+ def convert_video_codec(value)
52
+ "-vcodec #{value}"
53
+ end
54
+
55
+ def convert_frame_rate(value)
56
+ "-r #{value}"
57
+ end
58
+
59
+ def convert_resolution(value)
60
+ "-s #{value}"
61
+ end
62
+
63
+ def convert_video_bitrate(value)
64
+ "-b:v #{k_format(value)}"
65
+ end
66
+
67
+ def convert_audio_codec(value)
68
+ "-acodec #{value}"
69
+ end
70
+
71
+ def convert_audio_bitrate(value)
72
+ "-b:a #{k_format(value)}"
73
+ end
74
+
75
+ def convert_audio_sample_rate(value)
76
+ "-ar #{value}"
77
+ end
78
+
79
+ def convert_audio_channels(value)
80
+ "-ac #{value}"
81
+ end
82
+
83
+ def convert_video_max_bitrate(value)
84
+ "-maxrate #{k_format(value)}"
85
+ end
86
+
87
+ def convert_video_min_bitrate(value)
88
+ "-minrate #{k_format(value)}"
89
+ end
90
+
91
+ def convert_buffer_size(value)
92
+ "-bufsize #{k_format(value)}"
93
+ end
94
+
95
+ def convert_video_bitrate_tolerance(value)
96
+ "-bt #{k_format(value)}"
97
+ end
98
+
99
+ def convert_threads(value)
100
+ "-threads #{value}"
101
+ end
102
+
103
+ def convert_duration(value)
104
+ "-t #{value}"
105
+ end
106
+
107
+ def convert_video_preset(value)
108
+ "-vpre #{value}"
109
+ end
110
+
111
+ def convert_audio_preset(value)
112
+ "-apre #{value}"
113
+ end
114
+
115
+ def convert_file_preset(value)
116
+ "-fpre #{value}"
117
+ end
118
+
119
+ def convert_keyframe_interval(value)
120
+ "-g #{value}"
121
+ end
122
+
123
+ def convert_seek_time(value)
124
+ "-ss #{value}"
125
+ end
126
+
127
+ def convert_screenshot(value)
128
+ value ? "-vframes 1 -f image2" : ""
129
+ end
130
+
131
+ def convert_custom(value)
132
+ value
133
+ end
134
+
135
+ def k_format(value)
136
+ value.to_s.include?("k") ? value : "#{value}k"
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,4 @@
1
+ module FFMPEG
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,42 @@
1
+ require 'timeout'
2
+ require 'thread'
3
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
4
+ begin
5
+ require 'win32/process'
6
+ rescue LoadError
7
+ "Warning: streamio-ffmpeg is missing the win32-process gem to properly handle hung transcodings. Install the gem (in Gemfile if using bundler) to avoid errors."
8
+ end
9
+ end
10
+
11
+ #
12
+ # Monkey Patch timeout support into the IO class
13
+ #
14
+ class IO
15
+ def each_with_timeout(pid, seconds, sep_string=$/)
16
+ last_update = Time.now
17
+
18
+ current_thread = Thread.current
19
+ check_update_thread = Thread.new do
20
+ loop do
21
+ sleep 0.1
22
+ if last_update - Time.now < -seconds
23
+ current_thread.raise Timeout::Error.new('output wait time expired')
24
+ end
25
+ end
26
+ end
27
+
28
+ each(sep_string) do |buffer|
29
+ last_update = Time.now
30
+ yield buffer
31
+ end
32
+ rescue Timeout::Error
33
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
34
+ Process.kill(1, pid)
35
+ else
36
+ Process.kill('SIGKILL', pid)
37
+ end
38
+ raise
39
+ ensure
40
+ check_update_thread.kill
41
+ end
42
+ end
@@ -0,0 +1,206 @@
1
+ require 'time'
2
+
3
+ module FFMPEG
4
+ class Movie
5
+ attr_reader :path, :duration, :time, :bitrate, :rotation, :creation_time
6
+ attr_reader :video_stream, :video_codec, :video_bitrate, :colorspace, :resolution, :dar
7
+ attr_reader :audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate
8
+ attr_reader :container
9
+ attr_reader :streams
10
+
11
+ def initialize(path)
12
+ raise Errno::ENOENT, "the file '#{path}' does not exist" unless File.exists?(path)
13
+
14
+ @path = path
15
+
16
+ # ffmpeg will output to stderr
17
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
18
+ command = %Q[#{FFMPEG.ffmpeg_binary}" -i "#{path}]
19
+ else
20
+ command = "#{FFMPEG.ffmpeg_binary} -i #{Shellwords.escape(path)}"
21
+ end
22
+
23
+ output = Open3.popen3(command) { |stdin, stdout, stderr| stderr.read }
24
+
25
+ fix_encoding(output)
26
+
27
+ output[/Input \#\d+\,\s*(\S+),\s*from/]
28
+ @container = $1
29
+
30
+ output[/Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})/]
31
+ @duration = ($1.to_i*60*60) + ($2.to_i*60) + $3.to_f
32
+
33
+ output[/start: (\d*\.\d*)/]
34
+ @time = $1 ? $1.to_f : 0.0
35
+
36
+ output[/creation_time {1,}: {1,}(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/]
37
+ @creation_time = $1 ? Time.parse("#{$1}") : nil
38
+
39
+ output[/bitrate: (\d*)/]
40
+ @bitrate = $1 ? $1.to_i : nil
41
+
42
+ output[/rotate\ {1,}:\ {1,}(\d*)/]
43
+ @rotation = $1 ? $1.to_i : nil
44
+
45
+ output[/Video:\ (.*)/]
46
+ @video_stream = $1
47
+
48
+ output[/Audio:\ (.*)/]
49
+ @audio_stream = $1
50
+
51
+ if video_stream
52
+ @video_codec, @colorspace, resolution, video_bitrate = video_stream.split(/\s?,\s?/)
53
+ @video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
54
+ @resolution = resolution.split(" ").first rescue nil # get rid of [PAR 1:1 DAR 16:9]
55
+ @dar = $1 if video_stream[/DAR (\d+:\d+)/]
56
+ end
57
+
58
+ if audio_stream
59
+ @audio_codec, audio_sample_rate, @audio_channels, unused, audio_bitrate = audio_stream.split(/\s?,\s?/)
60
+ @audio_bitrate = audio_bitrate =~ %r(\A(\d+) kb/s(?: \(default\))?\Z) ? $1.to_i : nil
61
+ @audio_sample_rate = audio_sample_rate[/\d*/].to_i
62
+ end
63
+
64
+ @invalid = true if @video_stream.to_s.empty? && @audio_stream.to_s.empty?
65
+ @invalid = true if output.include?("is not supported")
66
+ @invalid = true if output.include?("could not find codec parameters")
67
+
68
+ load_all_streams output if valid?
69
+ end
70
+
71
+ def valid?
72
+ not @invalid
73
+ end
74
+
75
+ def width
76
+ resolution.split("x")[0].to_i rescue nil
77
+ end
78
+
79
+ def height
80
+ resolution.split("x")[1].to_i rescue nil
81
+ end
82
+
83
+ def calculated_aspect_ratio
84
+ aspect_from_dar || aspect_from_dimensions
85
+ end
86
+
87
+ def size
88
+ File.size(@path)
89
+ end
90
+
91
+ def audio_channels
92
+ return nil unless @audio_channels
93
+ return @audio_channels[/\d*/].to_i if @audio_channels["channels"]
94
+ return 1 if @audio_channels["mono"]
95
+ return 2 if @audio_channels["stereo"]
96
+ return 6 if @audio_channels["5.1"]
97
+ end
98
+
99
+ def frame_rate
100
+ return nil unless video_stream
101
+ video_stream[/(\d*\.?\d*)\s?fps/] ? $1.to_f : nil
102
+ end
103
+
104
+ def transcode(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
105
+ Transcoder.new(self, output_file, options, transcoder_options).run &block
106
+ end
107
+
108
+ def screenshot(output_file, options = EncodingOptions.new, transcoder_options = {}, &block)
109
+ Transcoder.new(self, output_file, options.merge(screenshot: true), transcoder_options).run &block
110
+ end
111
+
112
+ protected
113
+ def aspect_from_dar
114
+ return nil unless dar
115
+ w, h = dar.split(":")
116
+ aspect = w.to_f / h.to_f
117
+ aspect.zero? ? nil : aspect
118
+ end
119
+
120
+ def aspect_from_dimensions
121
+ aspect = width.to_f / height.to_f
122
+ aspect.nan? ? nil : aspect
123
+ end
124
+
125
+ def fix_encoding(output)
126
+ output[/test/] # Running a regexp on the string throws error if it's not UTF-8
127
+ rescue ArgumentError
128
+ output.force_encoding("ISO-8859-1")
129
+ end
130
+
131
+ def load_all_streams(ffmpeg_output)
132
+ @streams = ffmpeg_output.split(/.*? (?=(?:Chapter|Stream))/).map do |line|
133
+ if line =~ /^Stream #\d+:\d+/
134
+ begin
135
+ stream = FFMPEG::Stream.new
136
+ bogus, input_number, stream_number, language, type = line.match(/Stream #(\d+):(\d+)(?:\((.*?)\))?:\s*(\w+):(.*)\n/).to_a
137
+
138
+ stream.send(:input_number=, input_number)
139
+ stream.send(:stream_number=, stream_number)
140
+ if language
141
+ language_code = language.match(/(?<=\[)\w+(?=\])/)
142
+ if language_code
143
+ stream.send(:language=, language_code.to_a.first)
144
+ else
145
+ stream.send(:language=, language)
146
+ end
147
+ # else
148
+ # stream.send(:language=, :unk)
149
+ end
150
+ stream.send(:type=, type.downcase.gsub(/s$/, '').to_sym)
151
+
152
+ build_audio_stream stream, line if stream.type == :audio
153
+ build_video_stream stream, line if stream.type == :video
154
+ build_subtitle_stream stream, line if stream.type == :subtitle
155
+ rescue => e
156
+ ap [
157
+ e,
158
+ ffmpeg_output.split(/.*? (?=(?:Chapter|Stream))/),
159
+ line,
160
+ input_number, stream_number, language, type
161
+ ]
162
+
163
+ exit!
164
+ end
165
+ stream
166
+ else
167
+ nil
168
+ end
169
+ end
170
+
171
+ @streams = @streams.compact
172
+ end
173
+
174
+ def build_audio_stream(stream, line)
175
+ audio_codec, audio_sample_rate, audio_channels, unused, audio_bitrate = line.gsub(/^.*?Audio:\s/, '').split(/\s?,\s?/)
176
+ audio_bitrate = audio_bitrate =~ %r(\A(\d+) kb/s(?: \(default\))\Z) ? $1.to_i : nil
177
+ audio_sample_rate = audio_sample_rate[/\d*/].to_i
178
+
179
+ stream.send :audio_codec=, audio_codec
180
+ stream.send :audio_sample_rate=, audio_sample_rate
181
+ stream.send :audio_channels=, audio_channels
182
+ stream.send :audio_bitrate=, audio_bitrate
183
+ end
184
+
185
+ def build_video_stream(stream, line)
186
+ video_codec, colorspace, resolution, video_bitrate = line.gsub(/^.*?Video:\s/, '').split(/\s?,\s?/)
187
+ video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
188
+ resolution = resolution.split(" ").first rescue nil # get rid of [PAR 1:1 DAR 16:9]
189
+ dar = $1 if video_stream[/DAR (\d+:\d+)/]
190
+
191
+ stream.send :video_codec=, video_codec
192
+ stream.send :colorspace=, colorspace
193
+ stream.send :resolution=, resolution
194
+ stream.send :video_bitrate=, video_bitrate
195
+ stream.send :dar=, dar
196
+ end
197
+
198
+ def build_subtitle_stream(stream, line)
199
+ subtitle_codec = line.split("\n").first.gsub(/^.*?Subtitles?:\s/, '').strip
200
+ subtitle_format = subtitle_codec.split(/\s+/).first
201
+
202
+ stream.send :subtitle_codec=, subtitle_codec
203
+ stream.send :subtitle_format=, subtitle_format
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,15 @@
1
+ module FFMPEG
2
+ class Stream
3
+ attr_reader :input_number, :stream_number, :language, :type
4
+ attr_reader :video_codec, :video_bitrate, :colorspace, :resolution, :dar
5
+ attr_reader :audio_channels, :audio_codec, :audio_bitrate, :audio_sample_rate
6
+ attr_reader :subtitle_codec, :subtitle_format
7
+
8
+ private
9
+
10
+ attr_writer :input_number, :stream_number, :language, :type
11
+ attr_writer :video_codec, :video_bitrate, :colorspace, :resolution, :dar
12
+ attr_writer :audio_channels, :audio_codec, :audio_bitrate, :audio_sample_rate
13
+ attr_writer :subtitle_codec, :subtitle_format
14
+ end
15
+ end
@@ -0,0 +1,120 @@
1
+ require 'open3'
2
+ require 'shellwords'
3
+
4
+ module FFMPEG
5
+ class Transcoder
6
+ @@timeout = 30
7
+
8
+ def self.timeout=(time)
9
+ @@timeout = time
10
+ end
11
+
12
+ def self.timeout
13
+ @@timeout
14
+ end
15
+
16
+ def initialize(movie, output_file, options = EncodingOptions.new, transcoder_options = {})
17
+ @movie = movie
18
+ @output_file = output_file
19
+ @validate_output = transcoder_options[:validate_output] != false
20
+
21
+ if options.is_a?(String) || options.is_a?(EncodingOptions)
22
+ @raw_options = options
23
+ elsif options.is_a?(Hash)
24
+ @raw_options = EncodingOptions.new(options)
25
+ else
26
+ raise ArgumentError, "Unknown options format '#{options.class}', should be either EncodingOptions, Hash or String."
27
+ end
28
+
29
+ @transcoder_options = transcoder_options
30
+ @errors = []
31
+
32
+ apply_transcoder_options
33
+ end
34
+
35
+ # frame= 4855 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
36
+ def run
37
+
38
+ if RUBY_PLATFORM =~ /(win|w)(32|64)$/
39
+ command = %Q["#{FFMPEG.ffmpeg_binary}" -y -i "#{@movie.path}" #{@raw_options} "#{@output_file}"]
40
+ else
41
+ command = "#{FFMPEG.ffmpeg_binary} -y -i #{Shellwords.escape(@movie.path)} #{@raw_options} #{Shellwords.escape(@output_file)}"
42
+ end
43
+
44
+ FFMPEG.logger.info("Running transcoding...\n#{command}\n")
45
+ output = ""
46
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
47
+ begin
48
+ yield(0.0) if block_given?
49
+ next_line = Proc.new do |line|
50
+ fix_encoding(line)
51
+ output << line
52
+ if line.include?("time=")
53
+ if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
54
+ time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
55
+ else # better make sure it wont blow up in case of unexpected output
56
+ time = 0.0
57
+ end
58
+ progress = time / @movie.duration
59
+ yield(progress) if block_given?
60
+ end
61
+ end
62
+
63
+ if @@timeout
64
+ stderr.each_with_timeout(wait_thr.pid, @@timeout, 'size=', &next_line)
65
+ else
66
+ stderr.each('size=', &next_line)
67
+ end
68
+
69
+ rescue Timeout::Error => e
70
+ FFMPEG.logger.error "Process hung...\nCommand\n#{command}\nOutput\n#{output}\n"
71
+ raise Error, "Process hung. Full output: #{output}"
72
+ end
73
+ end
74
+
75
+ if encoding_succeeded?
76
+ yield(1.0) if block_given?
77
+ FFMPEG.logger.info "Transcoding of #{@movie.path} to #{@output_file} succeeded\n"
78
+ else
79
+ errors = "Errors: #{@errors.join(", ")}. "
80
+ FFMPEG.logger.error "Failed encoding...\n#{command}\n\n#{output}\n#{errors}\n"
81
+ raise Error, "Failed encoding.#{errors}Full output: #{output}"
82
+ end
83
+
84
+ encoded
85
+ end
86
+
87
+ def encoding_succeeded?
88
+ @errors << "no output file created" and return false unless File.exists?(@output_file)
89
+ (@errors << "encoded file is invalid" and return false unless encoded.valid?) if @validate_output
90
+ true
91
+ end
92
+
93
+ def encoded
94
+ @encoded ||= Movie.new(@output_file)
95
+ end
96
+
97
+ private
98
+ def apply_transcoder_options
99
+ return if @movie.calculated_aspect_ratio.nil?
100
+ case @transcoder_options[:preserve_aspect_ratio].to_s
101
+ when "width"
102
+ new_height = @raw_options.width / @movie.calculated_aspect_ratio
103
+ new_height = new_height.ceil.even? ? new_height.ceil : new_height.floor
104
+ new_height += 1 if new_height.odd? # needed if new_height ended up with no decimals in the first place
105
+ @raw_options[:resolution] = "#{@raw_options.width}x#{new_height}"
106
+ when "height"
107
+ new_width = @raw_options.height * @movie.calculated_aspect_ratio
108
+ new_width = new_width.ceil.even? ? new_width.ceil : new_width.floor
109
+ new_width += 1 if new_width.odd?
110
+ @raw_options[:resolution] = "#{new_width}x#{@raw_options.height}"
111
+ end
112
+ end
113
+
114
+ def fix_encoding(output)
115
+ output[/test/]
116
+ rescue ArgumentError
117
+ output.force_encoding("ISO-8859-1")
118
+ end
119
+ end
120
+ end