tootsie 0.9.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/.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
|