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.
- data/Gemfile +11 -0
- data/Gemfile.lock +44 -0
- data/README.md +3 -2
- data/bin/SublerCLI +0 -0
- data/gems/streamio-ffmpeg/CHANGELOG +198 -0
- data/gems/streamio-ffmpeg/Gemfile +3 -0
- data/gems/streamio-ffmpeg/LICENSE +20 -0
- data/gems/streamio-ffmpeg/README.md +180 -0
- data/gems/streamio-ffmpeg/Rakefile +22 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/encoding_options.rb +139 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/errors.rb +4 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/io_monkey.rb +42 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/movie.rb +206 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/stream.rb +15 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/transcoder.rb +120 -0
- data/gems/streamio-ffmpeg/lib/ffmpeg/version.rb +3 -0
- data/gems/streamio-ffmpeg/lib/streamio-ffmpeg.rb +49 -0
- data/gems/streamio-ffmpeg/spec/ffmpeg/encoding_options_spec.rb +135 -0
- data/gems/streamio-ffmpeg/spec/ffmpeg/movie_spec.rb +325 -0
- data/gems/streamio-ffmpeg/spec/ffmpeg/transcoder_spec.rb +210 -0
- data/gems/streamio-ffmpeg/spec/fixtures/bin/ffmpeg-audio-only +62 -0
- data/gems/streamio-ffmpeg/spec/fixtures/bin/ffmpeg-hanging +44 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome movie.mov +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome'movie.mov +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/awesome_widescreen.mov +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/broken.mp4 +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/empty.flv +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/sideways movie.mov +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/movies/weird_aspect.small.mpg +0 -0
- data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_iso-8859-1.txt +35 -0
- data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_no_audio.txt +18 -0
- data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_non_supported_audio.txt +29 -0
- data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_surround_sound.txt +19 -0
- data/gems/streamio-ffmpeg/spec/fixtures/outputs/file_with_weird_dar.txt +24 -0
- data/gems/streamio-ffmpeg/spec/fixtures/sounds/napoleon.mp3 +0 -0
- data/gems/streamio-ffmpeg/spec/spec_helper.rb +21 -0
- data/gems/streamio-ffmpeg/spec/streamio-ffmpeg_spec.rb +40 -0
- data/gems/streamio-ffmpeg/streamio-ffmpeg.gemspec +19 -0
- data/lib/apple_tv_converter.rb +10 -4
- data/lib/apple_tv_converter/apple_tv_converter.sublime-project +0 -6
- data/lib/apple_tv_converter/apple_tv_converter.sublime-workspace +501 -378
- data/lib/apple_tv_converter/command_line.rb +46 -22
- data/lib/apple_tv_converter/media.rb +49 -16
- data/lib/apple_tv_converter/media_converter.rb +19 -18
- data/lib/apple_tv_converter/media_converter_adapter.rb +58 -14
- data/lib/apple_tv_converter/media_converter_mac_adapter.rb +46 -35
- data/lib/apple_tv_converter/version.rb +1 -1
- 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,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
|