tootsie 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +15 -0
- data/Gemfile +2 -0
- data/License +7 -0
- data/README.md +256 -0
- data/Rakefile +1 -0
- data/Tootsie.gemspec +36 -0
- data/bin/tootsie_task_manager +82 -0
- data/config.ru +22 -0
- data/config/development-sample.yml +4 -0
- data/lib/tootsie.rb +21 -0
- data/lib/tootsie/application.rb +48 -0
- data/lib/tootsie/client.rb +12 -0
- data/lib/tootsie/command_runner.rb +58 -0
- data/lib/tootsie/configuration.rb +29 -0
- data/lib/tootsie/daemon.rb +282 -0
- data/lib/tootsie/ffmpeg_adapter.rb +132 -0
- data/lib/tootsie/image_metadata_extractor.rb +64 -0
- data/lib/tootsie/input.rb +55 -0
- data/lib/tootsie/output.rb +67 -0
- data/lib/tootsie/processors/image_processor.rb +181 -0
- data/lib/tootsie/processors/video_processor.rb +85 -0
- data/lib/tootsie/queues/file_system_queue.rb +65 -0
- data/lib/tootsie/queues/sqs_queue.rb +93 -0
- data/lib/tootsie/s3_utilities.rb +24 -0
- data/lib/tootsie/spawner.rb +99 -0
- data/lib/tootsie/task_manager.rb +51 -0
- data/lib/tootsie/tasks/job_task.rb +111 -0
- data/lib/tootsie/tasks/notify_task.rb +27 -0
- data/lib/tootsie/version.rb +3 -0
- data/lib/tootsie/web_service.rb +37 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/test_files/BF 0622 1820.tif +0 -0
- data/spec/tootsie/command_runner_spec.rb +29 -0
- data/spec/tootsie/image_metadata_extracter_spec.rb +39 -0
- data/spec/tootsie/s3_utilities_spec.rb +40 -0
- metadata +337 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
require 's3'
|
2
|
+
require 'sqs'
|
3
|
+
|
4
|
+
module Tootsie
|
5
|
+
|
6
|
+
class Application
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@@instance = self
|
10
|
+
@environment = options[:environment] || :development
|
11
|
+
@logger = options[:logger] || Logger.new($stderr)
|
12
|
+
@configuration = Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure!
|
16
|
+
@configuration.load_from_file(File.join(Dir.pwd, "config/#{@environment}.yml"))
|
17
|
+
@queue = Tootsie::SqsQueue.new(@configuration.sqs_queue_name, sqs_service)
|
18
|
+
@task_manager = TaskManager.new(@queue)
|
19
|
+
end
|
20
|
+
|
21
|
+
def s3_service
|
22
|
+
return @s3_service ||= ::S3::Service.new(
|
23
|
+
:access_key_id => @configuration.aws_access_key_id,
|
24
|
+
:secret_access_key => @configuration.aws_secret_access_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sqs_service
|
28
|
+
return @sqs_service ||= ::Sqs::Service.new(
|
29
|
+
:access_key_id => @configuration.aws_access_key_id,
|
30
|
+
:secret_access_key => @configuration.aws_secret_access_key)
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def get
|
35
|
+
@@instance
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :environment
|
40
|
+
|
41
|
+
attr_reader :configuration
|
42
|
+
attr_reader :task_manager
|
43
|
+
attr_reader :queue
|
44
|
+
attr_reader :logger
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tootsie
|
2
|
+
|
3
|
+
class CommandExecutionFailed < StandardError; end
|
4
|
+
|
5
|
+
class CommandRunner
|
6
|
+
|
7
|
+
def initialize(command_line, options = {})
|
8
|
+
@options = options.symbolize_keys
|
9
|
+
@options.assert_valid_keys(:ignore_exit_code, :output_encoding)
|
10
|
+
@command_line = command_line
|
11
|
+
@logger = Application.get.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(params = {}, &block)
|
15
|
+
command_line = @command_line
|
16
|
+
if params.any?
|
17
|
+
params = params.with_indifferent_access
|
18
|
+
command_line = command_line.gsub(/(^|\s):(\w+)/) do
|
19
|
+
pre, key, all = $1, $2, $~[0]
|
20
|
+
if params.include?(key)
|
21
|
+
value = params[key]
|
22
|
+
value = "'#{value}'" if value =~ /\s/
|
23
|
+
"#{pre}#{value}"
|
24
|
+
else
|
25
|
+
all
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
command_line = "#{command_line} 2>&1"
|
30
|
+
|
31
|
+
@logger.info("Running command: #{command_line}") if @logger.info?
|
32
|
+
IO.popen(command_line, "r:#{@options[:output_encoding] || 'utf-8'}") do |output|
|
33
|
+
output.each_line do |line|
|
34
|
+
@logger.info("[Command output] #{line.strip}") if @logger.info?
|
35
|
+
yield line if block_given?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
status = $?
|
39
|
+
if status.exited?
|
40
|
+
if status.exitstatus != 0
|
41
|
+
if @options[:ignore_exit_code]
|
42
|
+
return false
|
43
|
+
else
|
44
|
+
raise CommandExecutionFailed, "Command failed with exit code #{status.exitstatus}: #{command_line}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
elsif status.stopped?
|
48
|
+
raise CommandExecutionFailed, "Command stopped unexpectedly with signal #{status.stopsig}: #{command_line}"
|
49
|
+
elsif status.signaled?
|
50
|
+
raise CommandExecutionFailed, "Command died unexpectedly by signal #{status.termsig}: #{command_line}"
|
51
|
+
else
|
52
|
+
raise CommandExecutionFailed, "Command died unexpectedly: #{command_line}"
|
53
|
+
end
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Tootsie
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@ffmpeg_thread_count = 1
|
7
|
+
@sqs_queue_name = 'tootsie'
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_from_file(file_name)
|
11
|
+
config = (YAML.load(File.read(file_name)) || {}).with_indifferent_access
|
12
|
+
[:aws_access_key_id, :aws_secret_access_key, :ffmpeg_thread_count,
|
13
|
+
:sqs_queue_name].each do |key|
|
14
|
+
if config.include?(key)
|
15
|
+
value = config[key]
|
16
|
+
value = $1.to_i if value =~ /\A\s*(\d+)\s*\z/
|
17
|
+
instance_variable_set("@#{key}", value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :aws_access_key_id
|
23
|
+
attr_accessor :aws_secret_access_key
|
24
|
+
attr_accessor :ffmpeg_thread_count
|
25
|
+
attr_accessor :sqs_queue_name
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "fileutils"
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module Tootsie
|
6
|
+
|
7
|
+
class DaemonError < Exception; end
|
8
|
+
class DaemonAlreadyRunning < DaemonError; end
|
9
|
+
class DaemonNotRunning < DaemonError; end
|
10
|
+
class DaemonTerminationFailed < DaemonError; end
|
11
|
+
class DaemonNotConfigured < DaemonError; end
|
12
|
+
class DaemonStartFailed < DaemonError; end
|
13
|
+
|
14
|
+
# Daemon controller class that encapsulates a running daemon and a remote interface to it.
|
15
|
+
class Daemon
|
16
|
+
|
17
|
+
# Initializes the daemon controller.
|
18
|
+
def initialize(options = {})
|
19
|
+
@root = options[:root] || Dir.pwd
|
20
|
+
@pid_file = options[:pid_file]
|
21
|
+
@logger = options[:logger] || Logger.new('/dev/null')
|
22
|
+
@on_spawn = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Specifies a block to execute to run the actual daemon. Each call overrides the previous one.
|
26
|
+
def on_spawn(&block)
|
27
|
+
@on_spawn = block
|
28
|
+
end
|
29
|
+
|
30
|
+
# Specifies a block to execute to termiantion. Each call overrides the previous one.
|
31
|
+
def on_terminate(&block)
|
32
|
+
@on_terminate = block
|
33
|
+
end
|
34
|
+
|
35
|
+
# Control the daemon through command-line arguments.
|
36
|
+
def control(args, title = nil)
|
37
|
+
$stderr.sync = true
|
38
|
+
title ||= File.basename($0)
|
39
|
+
command = args.shift
|
40
|
+
control_with_command(command, args, title)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Control the daemon through a specific command.
|
44
|
+
def control_with_command(command, args, title = nil)
|
45
|
+
case command
|
46
|
+
when "start"
|
47
|
+
$stderr << "Starting #{title}: "
|
48
|
+
handle_errors do
|
49
|
+
start
|
50
|
+
$stderr << "started\n"
|
51
|
+
end
|
52
|
+
when "stop"
|
53
|
+
$stderr << "Stopping #{title}: "
|
54
|
+
options = {}
|
55
|
+
handle_errors do
|
56
|
+
stop({:signal => "TERM"}.merge(options))
|
57
|
+
$stderr << "stopped\n"
|
58
|
+
end
|
59
|
+
when "restart"
|
60
|
+
$stderr << "Restarting #{title}: "
|
61
|
+
handle_errors do
|
62
|
+
restart
|
63
|
+
$stderr << "restarted\n"
|
64
|
+
end
|
65
|
+
when "status"
|
66
|
+
if running?
|
67
|
+
$stderr << "#{title} is running\n"
|
68
|
+
else
|
69
|
+
$stderr << "#{title} is not running\n"
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if command
|
73
|
+
$stderr << "#{File.basename($0)}: Invalid command #{command}\n"
|
74
|
+
else
|
75
|
+
puts "Usage: #{File.basename($0)} [start | stop | restart | status]"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Starts daemon.
|
81
|
+
def start
|
82
|
+
raise DaemonNotConfigured, "Daemon not configured" unless @on_spawn
|
83
|
+
FileUtils.mkdir_p(File.dirname(@pid_file)) rescue nil
|
84
|
+
pid = self.pid
|
85
|
+
if pid
|
86
|
+
if pid_running?(pid)
|
87
|
+
raise DaemonAlreadyRunning, "Process is already running with pid #{pid}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
File.delete(@pid_file) rescue nil
|
91
|
+
child_pid = Process.fork
|
92
|
+
if child_pid
|
93
|
+
sleep(1)
|
94
|
+
unless running?
|
95
|
+
pid = self.pid
|
96
|
+
if pid == child_pid
|
97
|
+
raise DaemonStartFailed, "Daemon started, but failed prematurely"
|
98
|
+
else
|
99
|
+
raise DaemonStartFailed, "Daemon failed to start for some unknown reason"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return
|
103
|
+
end
|
104
|
+
class << logger
|
105
|
+
def format_message(severity, timestamp, progname, msg)
|
106
|
+
"[#{timestamp}] #{msg}\n"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
logger.info("Starting")
|
110
|
+
begin
|
111
|
+
Process.setsid
|
112
|
+
0.upto(255) do |n|
|
113
|
+
File.for_fd(n, "r").close rescue nil
|
114
|
+
end
|
115
|
+
File.umask(27)
|
116
|
+
Dir.chdir(@root)
|
117
|
+
$stdin = File.open("/dev/null", File::RDWR)
|
118
|
+
$stdout = File.open("/dev/null", File::RDWR)
|
119
|
+
$stderr = File.open("/dev/null", File::RDWR)
|
120
|
+
@pid = Process.pid
|
121
|
+
File.open(@pid_file, "w") do |file|
|
122
|
+
file << Process.pid
|
123
|
+
end
|
124
|
+
Signal.trap("HUP") do
|
125
|
+
logger.debug("Ignoring SIGHUP")
|
126
|
+
end
|
127
|
+
Signal.trap("TERM") do
|
128
|
+
if $$ == @pid
|
129
|
+
logger.info("Terminating (#{$$})")
|
130
|
+
@on_terminate.call if @on_terminate
|
131
|
+
File.delete(@pid_file) rescue nil
|
132
|
+
else
|
133
|
+
# Was sent to a child
|
134
|
+
end
|
135
|
+
exit(0)
|
136
|
+
end
|
137
|
+
@on_spawn.call
|
138
|
+
exit(0)
|
139
|
+
rescue SystemExit
|
140
|
+
# Do nothing
|
141
|
+
rescue Exception => e
|
142
|
+
message = "#{e.message}\n"
|
143
|
+
message << e.backtrace.map { |line| "\tfrom #{line}\n" }.join
|
144
|
+
logger.error(message)
|
145
|
+
exit(1)
|
146
|
+
ensure
|
147
|
+
logger.close
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Stops daemon.
|
152
|
+
def stop(options = {})
|
153
|
+
stopped = false
|
154
|
+
found = false
|
155
|
+
pid = self.pid
|
156
|
+
if pid
|
157
|
+
# Send TERM to process
|
158
|
+
begin
|
159
|
+
Process.kill(options[:signal] || "TERM", pid)
|
160
|
+
rescue Errno::ESRCH
|
161
|
+
stopped = true
|
162
|
+
rescue Exception => e
|
163
|
+
raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
|
164
|
+
end
|
165
|
+
unless stopped
|
166
|
+
# Process was signaled, now wait for it to die
|
167
|
+
found = true
|
168
|
+
30.times do
|
169
|
+
begin
|
170
|
+
if not pid_running?(pid)
|
171
|
+
stopped = true
|
172
|
+
break
|
173
|
+
end
|
174
|
+
sleep(1)
|
175
|
+
rescue Exception => e
|
176
|
+
raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
if found and not stopped
|
180
|
+
# Process still running after wait, force kill and wait
|
181
|
+
begin
|
182
|
+
Process.kill("KILL", pid)
|
183
|
+
rescue Errno::ESRCH
|
184
|
+
stopped = true
|
185
|
+
end
|
186
|
+
30.times do
|
187
|
+
begin
|
188
|
+
if not pid_running?(pid)
|
189
|
+
stopped = true
|
190
|
+
break
|
191
|
+
end
|
192
|
+
sleep(1)
|
193
|
+
rescue Exception => e
|
194
|
+
raise DaemonTerminationFailed, "Could not stop process #{pid}: #{e.message}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
if not stopped
|
198
|
+
raise DaemonTerminationFailed, "Timeout attempting to stop process #{pid}"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
unless found
|
204
|
+
raise DaemonNotRunning, "Process is not running"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Restarts daemon.
|
209
|
+
def restart
|
210
|
+
if running?
|
211
|
+
begin
|
212
|
+
stop
|
213
|
+
rescue DaemonNotRunning
|
214
|
+
# Ignore
|
215
|
+
end
|
216
|
+
end
|
217
|
+
start
|
218
|
+
end
|
219
|
+
|
220
|
+
# Is the daemon running?
|
221
|
+
def running?
|
222
|
+
!pid.nil?
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns the daemon pid.
|
226
|
+
def pid
|
227
|
+
pid = nil
|
228
|
+
maybe_pid = File.read(@pid_file) rescue nil
|
229
|
+
if maybe_pid =~ /([0-9]+)/
|
230
|
+
maybe_pid = $1.to_i
|
231
|
+
begin
|
232
|
+
Process.kill(0, maybe_pid)
|
233
|
+
rescue Errno::ESRCH
|
234
|
+
else
|
235
|
+
pid = maybe_pid
|
236
|
+
end
|
237
|
+
end
|
238
|
+
pid
|
239
|
+
end
|
240
|
+
|
241
|
+
# Signals the daemon.
|
242
|
+
def signal(signal)
|
243
|
+
pid = self.pid
|
244
|
+
if pid
|
245
|
+
Process.kill(signal, pid)
|
246
|
+
else
|
247
|
+
raise DaemonNotRunning, "Process is not running"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
attr_reader :root
|
252
|
+
attr_reader :pid_file
|
253
|
+
attr_reader :logger
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
def pid_running?(pid)
|
258
|
+
begin
|
259
|
+
Process.kill(0, pid)
|
260
|
+
rescue Errno::ESRCH
|
261
|
+
return false
|
262
|
+
end
|
263
|
+
return true
|
264
|
+
end
|
265
|
+
|
266
|
+
def handle_errors(&block)
|
267
|
+
begin
|
268
|
+
yield
|
269
|
+
rescue DaemonError => e
|
270
|
+
$stderr << "#{e.message}\n"
|
271
|
+
if e.is_a?(DaemonAlreadyRunning) or e.is_a?(DaemonNotRunning)
|
272
|
+
exit_code = 0
|
273
|
+
else
|
274
|
+
exit_code = 1
|
275
|
+
end
|
276
|
+
exit(exit_code)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Tootsie
|
2
|
+
|
3
|
+
class FfmpegAdapter
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@logger = Application.get.logger
|
7
|
+
@ffmpeg_binary = 'ffmpeg'
|
8
|
+
@ffmpeg_arguments = {}
|
9
|
+
@ffmpeg_arguments['threads'] = (options[:thread_count] || 1)
|
10
|
+
@ffmpeg_arguments['v'] = 1
|
11
|
+
if false
|
12
|
+
# TODO: Only in newer FFmpeg versions
|
13
|
+
@ffmpeg_arguments['xerror'] = true
|
14
|
+
@ffmpeg_arguments['loglevel'] = 'verbose'
|
15
|
+
end
|
16
|
+
@ffmpeg_arguments['y'] = true
|
17
|
+
end
|
18
|
+
|
19
|
+
# Transcode a file by taking an input file and writing an output file.
|
20
|
+
def transcode(input_filename, output_filename, options = {})
|
21
|
+
arguments = @ffmpeg_arguments.dup
|
22
|
+
if options[:audio_codec].to_s == 'none'
|
23
|
+
arguments['an'] = true
|
24
|
+
else
|
25
|
+
case options[:audio_codec].try(:to_s)
|
26
|
+
when 'aac'
|
27
|
+
arguments['acodec'] = 'libfaac'
|
28
|
+
when String
|
29
|
+
arguments['acodec'] = options[:audio_codec]
|
30
|
+
end
|
31
|
+
arguments['ar'] = options[:audio_sample_rate] if options[:audio_sample_rate]
|
32
|
+
arguments['ab'] = options[:audio_bitrate] if options[:audio_bitrate]
|
33
|
+
end
|
34
|
+
if options[:video_codec].to_s == 'none'
|
35
|
+
arguments['vn'] = true
|
36
|
+
else
|
37
|
+
case options[:video_codec].try(:to_s)
|
38
|
+
when 'h264'
|
39
|
+
arguments['vcodec'] = 'libx264'
|
40
|
+
arguments['vpre'] = ['medium', 'main'] # TODO: Allow override
|
41
|
+
arguments['crf'] = 15 # TODO: Allow override
|
42
|
+
arguments['threads'] = 0
|
43
|
+
when String
|
44
|
+
arguments['vcodec'] = options[:video_codec]
|
45
|
+
end
|
46
|
+
arguments['b'] = options[:video_bitrate] if options[:video_bitrate]
|
47
|
+
arguments['r'] = options[:video_frame_rate] if options[:video_frame_rate]
|
48
|
+
arguments['s'] = "#{options[:width]}x#{options[:height]}" if options[:width] or options[:height]
|
49
|
+
arguments['sameq'] = true
|
50
|
+
end
|
51
|
+
arguments['f'] = options[:format] if options[:format]
|
52
|
+
|
53
|
+
progress, expected_duration = @progress, nil
|
54
|
+
result_width, result_height = nil
|
55
|
+
run_ffmpeg(input_filename, output_filename, arguments) do |line|
|
56
|
+
if progress
|
57
|
+
case line
|
58
|
+
when /^\s*Duration: (\d+):(\d+):(\d+)\./
|
59
|
+
unless expected_duration
|
60
|
+
hours, minutes, seconds = $1.to_i, $2.to_i, $3.to_i
|
61
|
+
expected_duration = seconds + minutes * 60 + hours * 60 * 60
|
62
|
+
end
|
63
|
+
when /^frame=.* time=(\d+)\./
|
64
|
+
if expected_duration
|
65
|
+
elapsed_time = $1.to_i
|
66
|
+
end
|
67
|
+
when /Stream.*Video: .*, (\d+)x(\d+)\s/
|
68
|
+
unless result_width and result_height
|
69
|
+
result_width, result_height = $1.to_i, $2.to_i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
progress.call(elapsed_time, expected_duration) if elapsed_time
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
thumbnail_options = options[:thumbnail]
|
77
|
+
if thumbnail_options
|
78
|
+
thumb_width = thumbnail_options[:width].try(:to_i) || options[:width].try(:to_i)
|
79
|
+
thumb_height = thumbnail_options[:height].try(:to_i) || options[:height].try(:to_i)
|
80
|
+
if not thumbnail_options[:force_aspect_ratio] and result_width and result_height
|
81
|
+
thumb_height = (thumb_width / (result_width / result_height.to_f)).to_i
|
82
|
+
end
|
83
|
+
at_seconds = thumbnail_options[:at_seconds].try(:to_f)
|
84
|
+
at_seconds ||= (expected_duration || 0) * (thumbnail_options[:at_fraction].try(:to_f) || 0.5)
|
85
|
+
@logger.info("Getting thumbnail frame (#{thumb_width}x#{thumb_height}) with FFmpeg at #{at_seconds} seconds")
|
86
|
+
begin
|
87
|
+
run_ffmpeg(input_filename, thumbnail_options[:filename], @ffmpeg_arguments.merge(
|
88
|
+
:ss => at_seconds,
|
89
|
+
:vcodec => :mjpeg,
|
90
|
+
:vframes => 1,
|
91
|
+
:an => true,
|
92
|
+
:f => :rawvideo,
|
93
|
+
:s => "#{thumb_width}x#{thumb_height}"))
|
94
|
+
rescue FfmpegAdapterExecutionFailed => e
|
95
|
+
@logger.error("Thumbnail rendering failed, ignoring: #{e}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
attr_accessor :ffmpeg_binary
|
101
|
+
attr_accessor :ffmpeg_arguments
|
102
|
+
|
103
|
+
# Output captured from FFmpeg command line tool so far.
|
104
|
+
attr_reader :output
|
105
|
+
|
106
|
+
# Progress reporter that implements +call(seconds, total_seconds)+ to record
|
107
|
+
# transcoding progress.
|
108
|
+
attr_accessor :progress
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def run_ffmpeg(input_filename, output_filename, arguments, &block)
|
113
|
+
command_line = @ffmpeg_binary.dup
|
114
|
+
command_line << " -i '#{input_filename}' "
|
115
|
+
command_line << arguments.map { |k, v|
|
116
|
+
case v
|
117
|
+
when TrueClass, FalseClass
|
118
|
+
"-#{k}"
|
119
|
+
when Array
|
120
|
+
v.map { |w| "-#{k} '#{w}'" }.join(' ')
|
121
|
+
else
|
122
|
+
"-#{k} '#{v}'"
|
123
|
+
end
|
124
|
+
}.join(' ')
|
125
|
+
command_line << ' '
|
126
|
+
command_line << "'#{output_filename}'"
|
127
|
+
CommandRunner.new(command_line).run(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|