axtro-rvideo 0.9.6
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/CHANGELOG +70 -0
- data/ENV +100 -0
- data/ENV2 +129 -0
- data/LICENSE +20 -0
- data/Manifest +67 -0
- data/README +91 -0
- data/RULES +11 -0
- data/Rakefile +63 -0
- data/axtro-rvideo.gemspec +36 -0
- data/config/boot.rb +25 -0
- data/lib/rvideo.rb +44 -0
- data/lib/rvideo/errors.rb +24 -0
- data/lib/rvideo/float.rb +7 -0
- data/lib/rvideo/frame_capturer.rb +129 -0
- data/lib/rvideo/inspector.rb +483 -0
- data/lib/rvideo/reporter.rb +176 -0
- data/lib/rvideo/reporter/views/index.html.erb +27 -0
- data/lib/rvideo/reporter/views/report.css +27 -0
- data/lib/rvideo/reporter/views/report.html.erb +81 -0
- data/lib/rvideo/reporter/views/report.js +9 -0
- data/lib/rvideo/string.rb +5 -0
- data/lib/rvideo/tools/abstract_tool.rb +414 -0
- data/lib/rvideo/tools/ffmpeg.rb +286 -0
- data/lib/rvideo/tools/ffmpeg2theora.rb +42 -0
- data/lib/rvideo/tools/flvtool2.rb +50 -0
- data/lib/rvideo/tools/mencoder.rb +103 -0
- data/lib/rvideo/tools/mp4box.rb +21 -0
- data/lib/rvideo/tools/mp4creator.rb +35 -0
- data/lib/rvideo/tools/mplayer.rb +31 -0
- data/lib/rvideo/tools/qtfaststart.rb +37 -0
- data/lib/rvideo/tools/yamdi.rb +44 -0
- data/lib/rvideo/transcoder.rb +120 -0
- data/lib/rvideo/version.rb +9 -0
- data/rvideo.gemspec +36 -0
- data/scripts/txt2html +67 -0
- data/setup.rb +1585 -0
- data/spec/files/boat.avi +0 -0
- data/spec/files/kites.mp4 +0 -0
- data/spec/fixtures/ffmpeg_builds.yml +28 -0
- data/spec/fixtures/ffmpeg_results.yml +608 -0
- data/spec/fixtures/files.yml +398 -0
- data/spec/fixtures/recipes.yml +58 -0
- data/spec/integrations/formats_spec.rb +315 -0
- data/spec/integrations/frame_capturer_spec.rb +26 -0
- data/spec/integrations/inspection_spec.rb +112 -0
- data/spec/integrations/recipes_spec.rb +0 -0
- data/spec/integrations/rvideo_spec.rb +17 -0
- data/spec/integrations/transcoder_integration_spec.rb +29 -0
- data/spec/integrations/transcoding_spec.rb +9 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support.rb +36 -0
- data/spec/units/abstract_tool_spec.rb +123 -0
- data/spec/units/ffmpeg_spec.rb +327 -0
- data/spec/units/flvtool2_spec.rb +324 -0
- data/spec/units/frame_capturer_spec.rb +72 -0
- data/spec/units/inspector_spec.rb +59 -0
- data/spec/units/mencoder_spec.rb +4994 -0
- data/spec/units/mp4box_spec.rb +34 -0
- data/spec/units/mp4creator_spec.rb +34 -0
- data/spec/units/mplayer_spec.rb +34 -0
- data/spec/units/qtfaststart_spec.rb +35 -0
- data/spec/units/string_spec.rb +8 -0
- data/spec/units/transcoder_spec.rb +156 -0
- data/tasks/deployment.rake +5 -0
- data/tasks/testing.rake +27 -0
- data/tasks/transcoding.rake +40 -0
- data/tasks/website.rake +8 -0
- metadata +178 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
|
|
3
|
+
module RVideo
|
|
4
|
+
class Reporter
|
|
5
|
+
include ERB::Util
|
|
6
|
+
|
|
7
|
+
def self.run
|
|
8
|
+
Reporter.new.run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
@current_report_path = Reporter.next_available_report_path(File.join(REPORT_PATH, 'generated_reports'))
|
|
13
|
+
files = available_files
|
|
14
|
+
recipes = available_recipes
|
|
15
|
+
|
|
16
|
+
puts "\nInput files:\n--#{files.collect { |file| File.basename(file) }.join("\n--")}"
|
|
17
|
+
puts "\nInput recipes:\n--#{recipes.map {|name, recipe| name }.join("\n--")}"
|
|
18
|
+
combinations = calculate_combinations_using recipes, files
|
|
19
|
+
results = mass_transcode combinations
|
|
20
|
+
build_report_from results
|
|
21
|
+
puts "Done! Report available at #{@current_report_path}"
|
|
22
|
+
puts "Launching report in browser..."
|
|
23
|
+
exec "open #{@current_report_path}/index.html"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def self.next_available_report_path(base_path)
|
|
29
|
+
ordered_reports = Dir[File.join(base_path, "*")].sort_by {|name| File.basename(name).to_i }
|
|
30
|
+
ordered_reports = ["0"] if ordered_reports.empty?
|
|
31
|
+
last_report = File.basename(ordered_reports.last)
|
|
32
|
+
new_report_name = (last_report.to_i + 1).to_s
|
|
33
|
+
new_dir = File.join(base_path, new_report_name)
|
|
34
|
+
FileUtils.mkdir_p(new_dir)
|
|
35
|
+
new_dir
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def available_recipes
|
|
39
|
+
recipes = []
|
|
40
|
+
recipe_files = Dir[File.join(REPORT_PATH, "*.yml")]
|
|
41
|
+
recipe_files.each do |recipe_file|
|
|
42
|
+
YAML.load_file(recipe_file).each { |recipe| recipes << recipe }
|
|
43
|
+
end
|
|
44
|
+
if recipes.empty?
|
|
45
|
+
puts "No recipes found. Add recipe YAML files to report/."
|
|
46
|
+
exit
|
|
47
|
+
else
|
|
48
|
+
recipes
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def available_files
|
|
53
|
+
files = Dir[File.join(REPORT_PATH, "files/input/*.*")]
|
|
54
|
+
if files.empty?
|
|
55
|
+
puts "No input files. Add files to report/files/input to test."
|
|
56
|
+
exit
|
|
57
|
+
else
|
|
58
|
+
files
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def calculate_combinations_using(recipes, files)
|
|
63
|
+
@combinations = {}
|
|
64
|
+
files.each { |file| @combinations[file] = recipes }
|
|
65
|
+
@combinations
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_report_from(results, options = nil)
|
|
69
|
+
@results = results
|
|
70
|
+
#build main report
|
|
71
|
+
report = load_view 'index'
|
|
72
|
+
full_report_path = File.join(@current_report_path, "index.html")
|
|
73
|
+
File.open(full_report_path, "w+") do |file|
|
|
74
|
+
file.write report
|
|
75
|
+
end
|
|
76
|
+
#build individual reports
|
|
77
|
+
@results.each do |input_file, recipes|
|
|
78
|
+
recipes.each do |recipe_name, result|
|
|
79
|
+
build_individual_report(input_file, recipe_name, result)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_individual_report(input_file, recipe_name, result)
|
|
85
|
+
#instance variables may no longer be necessary...
|
|
86
|
+
@input_file = input_file
|
|
87
|
+
@recipe_name = recipe_name
|
|
88
|
+
@result = result
|
|
89
|
+
individual_report = load_view 'report'
|
|
90
|
+
individual_report_name = "#{underscoreize_file_basename(input_file)}_#{recipe_name}.html"
|
|
91
|
+
File.makedirs(File.join(@current_report_path, "individual_reports"))
|
|
92
|
+
full_report_path = File.join(@current_report_path, "individual_reports", individual_report_name)
|
|
93
|
+
File.open(full_report_path, "w+") do |file|
|
|
94
|
+
file.write individual_report
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_view(template_name)
|
|
100
|
+
template_file = "#{File.dirname(__FILE__)}/reporter/views/#{template_name}.html.erb"
|
|
101
|
+
template = File.read(template_file).gsub(/^ /, '')
|
|
102
|
+
ERB.new(template).result(binding)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def mass_transcode(combinations)
|
|
106
|
+
results = {}
|
|
107
|
+
combinations.each do |file, recipes|
|
|
108
|
+
results[file] = {}
|
|
109
|
+
recipes.each do |recipe_name, recipe|
|
|
110
|
+
puts "Transcoding #{File.basename(file)} using recipe #{recipe_name}"
|
|
111
|
+
|
|
112
|
+
#generate input/output file paths
|
|
113
|
+
input_file = File.expand_path(file)
|
|
114
|
+
output_file = generate_output_file_using input_file, recipe_name, recipe
|
|
115
|
+
#raise output_file
|
|
116
|
+
#input_file.gsub!(" ","\\ ")
|
|
117
|
+
input_file = "#{File.dirname(input_file)}/\"#{File.basename(input_file)}\""
|
|
118
|
+
|
|
119
|
+
#create logfile
|
|
120
|
+
log_file_name = underscoreize_file_basename(input_file) + "_" + recipe_name + ".log"
|
|
121
|
+
log_file = create_log_file(log_file_name)
|
|
122
|
+
RVideo.logger = Logger.new(log_file)
|
|
123
|
+
|
|
124
|
+
transcoder, errors = transcode(recipe, input_file, output_file)
|
|
125
|
+
|
|
126
|
+
#build the results object for the views
|
|
127
|
+
results[file][recipe_name] = {}
|
|
128
|
+
results[file][recipe_name]['output_file'] = output_file
|
|
129
|
+
results[file][recipe_name]['transcoder'] = transcoder
|
|
130
|
+
results[file][recipe_name]['errors'] = errors
|
|
131
|
+
results[file][recipe_name]['recipe'] = recipe
|
|
132
|
+
results[file][recipe_name]['log'] = log_file
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
return results
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def generate_output_file_using(selected_file, recipe_name, recipe)
|
|
139
|
+
#File.join(@current_report_path, 'output_files')
|
|
140
|
+
output_path = File.join(@current_report_path, 'output_files' + underscoreize_file_basename(selected_file))
|
|
141
|
+
File.makedirs output_path
|
|
142
|
+
output_filename = "#{recipe_name}.#{recipe['extension']}"
|
|
143
|
+
output_file = File.join(output_path, output_filename)
|
|
144
|
+
#output_file.gsub(" ","_")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def underscoreize_file_basename(file)
|
|
148
|
+
File.basename(file).gsub(".","_").gsub(" ","_")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def transcode(recipe, input_file, output_file)
|
|
152
|
+
command = recipe['command']
|
|
153
|
+
errors = nil
|
|
154
|
+
|
|
155
|
+
#RVideo::Transcoder.logger = Logger.new(STDOUT)
|
|
156
|
+
begin
|
|
157
|
+
transcoder = RVideo::Transcoder.new
|
|
158
|
+
transcoder.execute(command, {:input_file => input_file,
|
|
159
|
+
:output_file => output_file})
|
|
160
|
+
#rescue => errors
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
return transcoder, errors
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def create_log_file(log_file_name)
|
|
167
|
+
log_path = File.join(@current_report_path, "logs")
|
|
168
|
+
File.makedirs log_path
|
|
169
|
+
logfile = File.join(log_path, log_file_name)
|
|
170
|
+
File.open(logfile, "w+") { |file| }
|
|
171
|
+
logfile
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>RVideo Reports Index</title>
|
|
4
|
+
<link rel="stylesheet" href="report.css" type="text/css" media="screen" />
|
|
5
|
+
<script type="text/javascript" src="report.js"></script>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<% @results.each do |input_file, recipes| %>
|
|
9
|
+
<h1>
|
|
10
|
+
<a href="<%= input_file %>"><%= File.basename(input_file) %> (launch file)</a>
|
|
11
|
+
</h1>
|
|
12
|
+
<ol>
|
|
13
|
+
<% recipes.each do |recipe_name, result| %>
|
|
14
|
+
<li>
|
|
15
|
+
<% css_class = 'warning' unless result['transcoder'].errors.empty? %>
|
|
16
|
+
<% css_class = 'critical' if result['errors'] %>
|
|
17
|
+
<% css_class = 'passed' if css_class.nil? %>
|
|
18
|
+
<div class="<%= css_class %>">
|
|
19
|
+
<% individual_report_url = "individual_reports/" + underscoreize_file_basename(input_file) + "_" + recipe_name + ".html" %>
|
|
20
|
+
<h2><a href="<%= result['output_file'] %>">Launch <%= recipe_name %></a> <a class="view-report" href="<%= individual_report_url %>" >view full report</a></h2>
|
|
21
|
+
</div>
|
|
22
|
+
</li>
|
|
23
|
+
<% end %>
|
|
24
|
+
</ol>
|
|
25
|
+
<% end %>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* a { color: black; margin-left: 20px;}
|
|
2
|
+
a:visited { color: #111; }
|
|
3
|
+
.critical { background-color: #E6E6E6; border-left: 20px solid #F00; }
|
|
4
|
+
.warning { background-color: #E6E6E6; border-left: 20px solid orange; }
|
|
5
|
+
.passed { background-color: #E6E6E6; border-left: 20px solid #0F0; }
|
|
6
|
+
|
|
7
|
+
div { margin: 10px; border: 1px solid #ccc; padding: 5px; }
|
|
8
|
+
li { margin: 20px; padding: 5px; list-style-type: none; }
|
|
9
|
+
span { background-color: #ccc; padding: 5px; width: 500px;}
|
|
10
|
+
span:hover { cursor: pointer; text-decoration: underline; }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
html { font-size: .75em;}
|
|
14
|
+
div { font-size: 1.2em;}
|
|
15
|
+
h1, h2, h3, h4, h5, ul { margin: .5em; }
|
|
16
|
+
a { color: black; margin-left: 2em; padding: 1em;}
|
|
17
|
+
a:visited { color: #111; }
|
|
18
|
+
.critical, .warning, .passed { background-color: #E6E6E6; border-left: 5em solid; }
|
|
19
|
+
.critical { border-left-color: #F00; }
|
|
20
|
+
.warning { border-left-color: orange; }
|
|
21
|
+
.passed { border-left-color: #0F0; }
|
|
22
|
+
|
|
23
|
+
div { margin: 1em; border: 1px solid #ccc; padding: .5em; }
|
|
24
|
+
ol li { margin: .01em; padding: 0; margin-left: 5em; }
|
|
25
|
+
ul li { margin: 1em; padding: 1em; list-style-type: none; }
|
|
26
|
+
span { background-color: #ccc; padding: 1em; width: 500px;}
|
|
27
|
+
span:hover { cursor: pointer; text-decoration: underline; }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>RVideo Reports</title>
|
|
4
|
+
<link rel="stylesheet" href="<%= report.css %>" type="text/css" media="screen" />
|
|
5
|
+
<script type="text/javascript" src="<%= report.js %>"></script>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div>
|
|
9
|
+
<h2><a href="<%= @result['output_file'] %>">Launch output file</a></h2>
|
|
10
|
+
<ul>
|
|
11
|
+
<li>
|
|
12
|
+
<div>
|
|
13
|
+
<h2>Recipe: <%= @recipe_name %></h2>
|
|
14
|
+
<p>
|
|
15
|
+
<% @result['recipe'].each do |key, value|%>
|
|
16
|
+
<%= key %>: <%= value %><br />
|
|
17
|
+
<% end %>
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</li>
|
|
21
|
+
<% unless @result['errors'].nil? %>
|
|
22
|
+
<li>
|
|
23
|
+
<span onclick="toggle('rescued-errors');">Hide/Show Rescued Errors</span>
|
|
24
|
+
<div id='rescued-errors' style="display: none;">
|
|
25
|
+
<h2>Rescued Error Backtrace</h2>
|
|
26
|
+
<h4><%= h(@result['errors'].class.name) %></h4>
|
|
27
|
+
<p><%= h(@result['errors'].message)%>
|
|
28
|
+
<p><%= h(@result['errors'].backtrace) %></p>
|
|
29
|
+
</div>
|
|
30
|
+
</li>
|
|
31
|
+
<% end %>
|
|
32
|
+
<li>
|
|
33
|
+
<span onclick="toggle('transcoder');">Hide/Show Transcoder</span>
|
|
34
|
+
<div id='transcoder' style="display: none;">
|
|
35
|
+
<h2>Transcoder</h2>
|
|
36
|
+
<% unless @result['transcoder'].errors.empty? %>
|
|
37
|
+
<h3>Transcoder Errors</h3>
|
|
38
|
+
<p><%= h(@result['transcoder'].errors.inspect) %></p>
|
|
39
|
+
<% end %>
|
|
40
|
+
<h3>Executed Commands</h3>
|
|
41
|
+
<p><%= h(@result['transcoder'].executed_commands.map(&:command)) %></p>
|
|
42
|
+
<h3>Raw Meta</h3>
|
|
43
|
+
<p><%= h(@result['transcoder'].metadata) %></p>
|
|
44
|
+
</div>
|
|
45
|
+
<li>
|
|
46
|
+
<span onclick="toggle('input-file');">Hide/Show Input File</span>
|
|
47
|
+
<div id='input-file' style="display: none;">
|
|
48
|
+
<h2>Original Input File</h2>
|
|
49
|
+
<h3>Raw Metadata</h3>
|
|
50
|
+
<p><%= h(@result['transcoder'].original.raw_metadata) %></p>
|
|
51
|
+
<h3>Raw Response</h3>
|
|
52
|
+
<p><%= h(@result['transcoder'].original.raw_response) %></p>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
<% if @result['transcoder'].processed %>
|
|
56
|
+
<li>
|
|
57
|
+
<span onclick="toggle('output-file');">Hide/Show Output File</span>
|
|
58
|
+
<div id='output-file' style="display: none;">
|
|
59
|
+
<h2>Processed Output File</h2>
|
|
60
|
+
<h3>Raw Metadata</h3>
|
|
61
|
+
<p><%= h(@result['transcoder'].processed.raw_metadata) %></p>
|
|
62
|
+
<h3>Raw Response</h3>
|
|
63
|
+
<p><%= h(@result['transcoder'].processed.raw_response) %></p>
|
|
64
|
+
</div>
|
|
65
|
+
</li>
|
|
66
|
+
<% end %>
|
|
67
|
+
<li>
|
|
68
|
+
<span onclick="toggle('log');">Hide/Show Log</span>
|
|
69
|
+
<div id='log' style="display: none;">
|
|
70
|
+
<h2>Log</h2>
|
|
71
|
+
<p>
|
|
72
|
+
<% File.readlines(@result['log']).each do |line| %>
|
|
73
|
+
<%= line %> <br />
|
|
74
|
+
<% end %>
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
</li>
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
module RVideo # :nodoc:
|
|
2
|
+
module Tools # :nodoc:
|
|
3
|
+
|
|
4
|
+
# AbstractTool is an interface to every transcoder tool class
|
|
5
|
+
# (e.g. ffmpeg, flvtool2). Called by the Transcoder class.
|
|
6
|
+
class AbstractTool
|
|
7
|
+
|
|
8
|
+
def self.assign(cmd, options = {})
|
|
9
|
+
tool_name = cmd.split(" ").first
|
|
10
|
+
begin
|
|
11
|
+
tool = "RVideo::Tools::#{tool_name.underscore.classify}".constantize.send(:new, cmd, options)
|
|
12
|
+
# rescue NameError, /uninitialized constant/
|
|
13
|
+
# raise TranscoderError::UnknownTool, "The recipe tried to use the '#{tool_name}' tool, which does not exist."
|
|
14
|
+
rescue => e
|
|
15
|
+
RVideo.logger.info e.message
|
|
16
|
+
RVideo.logger.info e.backtrace.join("\n")
|
|
17
|
+
raise e
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
module InstanceMethods
|
|
23
|
+
# Defines abstract methods in the convention of "format_#{attribute}"
|
|
24
|
+
# which are meant to be redefined by classes including this behavior.
|
|
25
|
+
def self.abstract_attribute_formatter(*names)
|
|
26
|
+
names.map { |n| "format_#{n}" }.each do |name|
|
|
27
|
+
class_eval %{
|
|
28
|
+
def #{name}(params = {})
|
|
29
|
+
raise ParameterError,
|
|
30
|
+
"The #{self.class} tool has not implemented the :#{name} method."
|
|
31
|
+
end
|
|
32
|
+
}, __FILE__, __LINE__
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
abstract_attribute_formatter :resolution, :deinterlace, :fps,
|
|
37
|
+
:video_bit_rate, :video_bit_rate_tolerance,
|
|
38
|
+
:video_bit_rate_min, :video_bit_rate_max,
|
|
39
|
+
:audio_channels, :audio_bit_rate, :audio_sample_rate
|
|
40
|
+
|
|
41
|
+
###
|
|
42
|
+
|
|
43
|
+
attr_reader :options, :command, :raw_result
|
|
44
|
+
attr_writer :original
|
|
45
|
+
|
|
46
|
+
def initialize(raw_command, options = {})
|
|
47
|
+
@raw_command = raw_command
|
|
48
|
+
@options = HashWithIndifferentAccess.new(options)
|
|
49
|
+
@command = interpolate_variables(raw_command)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute
|
|
53
|
+
@output_params = {}
|
|
54
|
+
|
|
55
|
+
# Dump the log output into a temp file
|
|
56
|
+
log_temp_file_name = "/tmp/transcode_output_#{Time.now.to_i}.txt"
|
|
57
|
+
|
|
58
|
+
final_command = "#{@command} 2>#{log_temp_file_name}"
|
|
59
|
+
|
|
60
|
+
# nice the command if option :nice was given
|
|
61
|
+
# accepts a number 1..19 (nice value) or anything evaluating to true
|
|
62
|
+
unless RUBY_PLATFORM.downcase.include?("mswin")
|
|
63
|
+
if @options.has_key? 'nice'
|
|
64
|
+
if (1..19) === @options['nice']
|
|
65
|
+
final_command = "nice -n#{@options['nice']} #{final_command}"
|
|
66
|
+
elsif @options['nice']
|
|
67
|
+
final_command = "nice #{final_command}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
RVideo.logger.info("\nExecuting Command: #{final_command}\n")
|
|
73
|
+
do_execute final_command
|
|
74
|
+
|
|
75
|
+
populate_raw_result(log_temp_file_name)
|
|
76
|
+
|
|
77
|
+
RVideo.logger.info("Result: \n#{@raw_result}")
|
|
78
|
+
parse_result(@raw_result)
|
|
79
|
+
|
|
80
|
+
# Cleanup log file
|
|
81
|
+
begin
|
|
82
|
+
File.delete(log_temp_file_name)
|
|
83
|
+
rescue Exception => e
|
|
84
|
+
RVideo.logger.error("Failed to delete output log file: #{log_temp_file_name}, e=#{e}")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Wrapper around the system call, for whenever we need to
|
|
89
|
+
# hook on or redefine this without messing with Kernel
|
|
90
|
+
def do_execute(command)
|
|
91
|
+
system command
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# Magic parameters
|
|
96
|
+
#
|
|
97
|
+
def temp_dir
|
|
98
|
+
if @options['output_file']
|
|
99
|
+
"#{File.dirname(@options['output_file'])}/"
|
|
100
|
+
else
|
|
101
|
+
""
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
###
|
|
106
|
+
# FPS aka framerate
|
|
107
|
+
|
|
108
|
+
def fps
|
|
109
|
+
format_fps(get_fps)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_fps
|
|
113
|
+
inspect_original if @original.nil?
|
|
114
|
+
fps = @options['fps'] || ""
|
|
115
|
+
case fps
|
|
116
|
+
when "copy"
|
|
117
|
+
get_original_fps
|
|
118
|
+
else
|
|
119
|
+
get_specific_fps
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def get_original_fps
|
|
124
|
+
return {} if @original.fps.nil?
|
|
125
|
+
{ :fps => @original.fps }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def get_specific_fps
|
|
129
|
+
{ :fps => @options['fps'] }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
###
|
|
133
|
+
# Resolution
|
|
134
|
+
|
|
135
|
+
def deinterlace
|
|
136
|
+
format_deinterlace(get_deinterlace)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def get_deinterlace
|
|
140
|
+
{ :deinterlace => @options['deinterlace'] ? true : false }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def resolution
|
|
144
|
+
format_resolution(get_resolution)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def get_resolution
|
|
148
|
+
inspect_original if @original.nil?
|
|
149
|
+
|
|
150
|
+
case @options['resolution']
|
|
151
|
+
when "copy" then get_original_resolution
|
|
152
|
+
when "width" then get_fit_to_width_resolution
|
|
153
|
+
when "height" then get_fit_to_height_resolution
|
|
154
|
+
when "letterbox" then get_letterbox_resolution
|
|
155
|
+
else
|
|
156
|
+
if @options["width"] and not @options["height"]
|
|
157
|
+
get_fit_to_width_resolution
|
|
158
|
+
elsif @options["height"] and not @options["width"]
|
|
159
|
+
get_fit_to_height_resolution
|
|
160
|
+
elsif @options["width"] and @options["height"]
|
|
161
|
+
get_specific_resolution
|
|
162
|
+
else
|
|
163
|
+
get_original_resolution
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def get_fit_to_width_resolution
|
|
169
|
+
w = @options['width']
|
|
170
|
+
|
|
171
|
+
raise TranscoderError::ParameterError,
|
|
172
|
+
"invalid width of '#{w}' for fit to width" unless valid_dimension?(w)
|
|
173
|
+
|
|
174
|
+
h = calculate_height(@original.width, @original.height, w)
|
|
175
|
+
|
|
176
|
+
{ :scale => { :width => w, :height => h } }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def get_fit_to_height_resolution
|
|
180
|
+
h = @options['height']
|
|
181
|
+
|
|
182
|
+
raise TranscoderError::ParameterError,
|
|
183
|
+
"invalid height of '#{h}' for fit to height" unless valid_dimension?(h)
|
|
184
|
+
|
|
185
|
+
w = calculate_width(@original.width, @original.height, h)
|
|
186
|
+
|
|
187
|
+
{ :scale => { :width => w, :height => h } }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def get_letterbox_resolution
|
|
191
|
+
lw = @options['width'].to_i
|
|
192
|
+
lh = @options['height'].to_i
|
|
193
|
+
|
|
194
|
+
raise TranscoderError::ParameterError,
|
|
195
|
+
"invalid width of '#{lw}' for letterbox" unless valid_dimension?(lw)
|
|
196
|
+
raise TranscoderError::ParameterError,
|
|
197
|
+
"invalid height of '#{lh}' for letterbox" unless valid_dimension?(lh)
|
|
198
|
+
|
|
199
|
+
w = calculate_width(@original.width, @original.height, lh)
|
|
200
|
+
h = calculate_height(@original.width, @original.height, lw)
|
|
201
|
+
|
|
202
|
+
if w > lw
|
|
203
|
+
w = lw
|
|
204
|
+
h = calculate_height(@original.width, @original.height, lw)
|
|
205
|
+
else
|
|
206
|
+
h = lh
|
|
207
|
+
w = calculate_width(@original.width, @original.height, lh)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
{ :scale => { :width => w, :height => h },
|
|
211
|
+
:letterbox => { :width => lw, :height => lh } }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def get_original_resolution
|
|
215
|
+
{ :scale => { :width => @original.width, :height => @original.height } }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def get_specific_resolution
|
|
219
|
+
w = @options['width']
|
|
220
|
+
h = @options['height']
|
|
221
|
+
|
|
222
|
+
raise TranscoderError::ParameterError,
|
|
223
|
+
"invalid width of '#{w}' for specific resolution" unless valid_dimension?(w)
|
|
224
|
+
raise TranscoderError::ParameterError,
|
|
225
|
+
"invalid height of '#{h}' for specific resolution" unless valid_dimension?(h)
|
|
226
|
+
|
|
227
|
+
{ :scale => { :width => w, :height => h } }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def calculate_width(ow, oh, h)
|
|
231
|
+
w = ((ow.to_f / oh.to_f) * h.to_f).to_i
|
|
232
|
+
(w.to_f / 16).round * 16
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def calculate_height(ow, oh, w)
|
|
236
|
+
h = (w.to_f / (ow.to_f / oh.to_f)).to_i
|
|
237
|
+
(h.to_f / 16).round * 16
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def valid_dimension?(dim)
|
|
241
|
+
dim.to_i > 0
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
###
|
|
245
|
+
# Audio channels
|
|
246
|
+
|
|
247
|
+
def audio_channels
|
|
248
|
+
format_audio_channels(get_audio_channels)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def get_audio_channels
|
|
252
|
+
channels = @options['audio_channels'] || ""
|
|
253
|
+
case channels
|
|
254
|
+
when "stereo"
|
|
255
|
+
get_stereo_audio
|
|
256
|
+
when "mono"
|
|
257
|
+
get_mono_audio
|
|
258
|
+
else
|
|
259
|
+
{}
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def get_stereo_audio
|
|
264
|
+
{ :channels => "2" }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def get_mono_audio
|
|
268
|
+
{ :channels => "1" }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def get_specific_audio_bit_rate
|
|
272
|
+
{ :bit_rate => @options['audio_bit_rate'] }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def get_specific_audio_sample_rate
|
|
276
|
+
{ :sample_rate => @options['audio_sample_rate'] }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
###
|
|
280
|
+
# Audio bit rate
|
|
281
|
+
|
|
282
|
+
def audio_bit_rate
|
|
283
|
+
format_audio_bit_rate(get_audio_bit_rate)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def get_audio_bit_rate
|
|
287
|
+
bit_rate = @options['audio_bit_rate'] || ""
|
|
288
|
+
case bit_rate
|
|
289
|
+
when ""
|
|
290
|
+
{}
|
|
291
|
+
else
|
|
292
|
+
get_specific_audio_bit_rate
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
###
|
|
297
|
+
# Audio sample rate
|
|
298
|
+
|
|
299
|
+
def audio_sample_rate
|
|
300
|
+
format_audio_sample_rate(get_audio_sample_rate)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def get_audio_sample_rate
|
|
304
|
+
sample_rate = @options['audio_sample_rate'] || ""
|
|
305
|
+
case sample_rate
|
|
306
|
+
when ""
|
|
307
|
+
{}
|
|
308
|
+
else
|
|
309
|
+
get_specific_audio_sample_rate
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
###
|
|
314
|
+
# Video quality
|
|
315
|
+
|
|
316
|
+
def video_quality
|
|
317
|
+
format_video_quality(get_video_quality)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def get_video_quality
|
|
321
|
+
quality = @options['video_quality'] || 'medium'
|
|
322
|
+
|
|
323
|
+
{ :video_quality => quality }.
|
|
324
|
+
merge!(get_fps).
|
|
325
|
+
merge!(get_resolution).
|
|
326
|
+
merge!(get_video_bit_rate)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def video_bit_rate
|
|
330
|
+
format_video_bit_rate(get_video_bit_rate)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def get_video_bit_rate
|
|
334
|
+
{ :video_bit_rate => @options["video_bit_rate"] }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def video_bit_rate_tolerance
|
|
338
|
+
format_video_bit_rate_tolerance(get_video_bit_rate_tolerance)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def get_video_bit_rate_tolerance
|
|
342
|
+
{ :video_bit_rate_tolerance => @options["video_bit_rate_tolerance"] }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def video_bit_rate_min
|
|
346
|
+
format_video_bit_rate_min(get_video_bit_rate_min)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def get_video_bit_rate_min
|
|
350
|
+
{ :video_bit_rate_min => @options["video_bit_rate_min"] }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def video_bit_rate_max
|
|
354
|
+
format_video_bit_rate_max(get_video_bit_rate_max)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def get_video_bit_rate_max
|
|
358
|
+
{ :video_bit_rate_max => @options["video_bit_rate_max"] }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
VARIABLE_INTERPOLATION_SCAN_PATTERN = /[^\\]\$[-_a-zA-Z]+\$/
|
|
364
|
+
|
|
365
|
+
def interpolate_variables(raw_command)
|
|
366
|
+
raw_command.scan(VARIABLE_INTERPOLATION_SCAN_PATTERN).each do |match|
|
|
367
|
+
match = match[0..0] == "$" ? match : match[1..(match.size - 1)]
|
|
368
|
+
match.strip!
|
|
369
|
+
|
|
370
|
+
value = if ["$input_file$", "$output_file$"].include?(match)
|
|
371
|
+
matched_variable(match).to_s.shell_quoted
|
|
372
|
+
else
|
|
373
|
+
matched_variable(match).to_s
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
raw_command.gsub!(match, value)
|
|
377
|
+
end
|
|
378
|
+
raw_command.gsub("\\$", "$")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
#
|
|
382
|
+
# Strip the $s. First, look for a supplied option that matches the
|
|
383
|
+
# variable name. If one is not found, look for a method that matches.
|
|
384
|
+
# If not found, raise ParameterError exception.
|
|
385
|
+
#
|
|
386
|
+
|
|
387
|
+
def matched_variable(match)
|
|
388
|
+
variable_name = match.gsub("$","")
|
|
389
|
+
if self.respond_to? variable_name
|
|
390
|
+
self.send(variable_name)
|
|
391
|
+
elsif @options.key?(variable_name)
|
|
392
|
+
@options[variable_name]
|
|
393
|
+
else
|
|
394
|
+
raise TranscoderError::ParameterError,
|
|
395
|
+
"command is looking for the #{variable_name} parameter, but it was not provided. (Command: #{@raw_command})"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def inspect_original
|
|
401
|
+
@original = Inspector.new(:file => options[:input_file])
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Pulls the interesting bits of the temp log file into memory. This is fairly tool-specific, so
|
|
405
|
+
# it's doubtful that this default version is going to work without being overridded.
|
|
406
|
+
def populate_raw_result(temp_file_name)
|
|
407
|
+
@raw_result = `tail -n 500 #{temp_file_name}`
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
end # InstanceMethods
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
end
|
|
414
|
+
end
|