rvideo 0.8.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.
Files changed (45) hide show
  1. data/ENV +100 -0
  2. data/ENV2 +129 -0
  3. data/History.txt +3 -0
  4. data/License.txt +20 -0
  5. data/Manifest.txt +44 -0
  6. data/README.txt +91 -0
  7. data/RULES +11 -0
  8. data/Rakefile +163 -0
  9. data/config/boot.rb +16 -0
  10. data/lib/rvideo.rb +19 -0
  11. data/lib/rvideo/errors.rb +21 -0
  12. data/lib/rvideo/inspector.rb +486 -0
  13. data/lib/rvideo/reporter.rb +176 -0
  14. data/lib/rvideo/reporter/views/index.html.erb +27 -0
  15. data/lib/rvideo/reporter/views/report.css +27 -0
  16. data/lib/rvideo/reporter/views/report.html.erb +81 -0
  17. data/lib/rvideo/reporter/views/report.js +9 -0
  18. data/lib/rvideo/tools/abstract_tool.rb +79 -0
  19. data/lib/rvideo/tools/ffmpeg.rb +111 -0
  20. data/lib/rvideo/tools/flvtool2.rb +45 -0
  21. data/lib/rvideo/transcoder.rb +113 -0
  22. data/lib/rvideo/version.rb +9 -0
  23. data/scripts/txt2html +67 -0
  24. data/setup.rb +1585 -0
  25. data/spec/files/kites.mp4 +0 -0
  26. data/spec/fixtures/ffmpeg_builds.yml +28 -0
  27. data/spec/fixtures/files.yml +374 -0
  28. data/spec/fixtures/recipes.yml +6 -0
  29. data/spec/integrations/files/files.yml +361 -0
  30. data/spec/integrations/formats_spec.rb +287 -0
  31. data/spec/integrations/inspection_spec.rb +15 -0
  32. data/spec/integrations/recipes_spec.rb +0 -0
  33. data/spec/integrations/rvideo_spec.rb +17 -0
  34. data/spec/integrations/transcoding_spec.rb +9 -0
  35. data/spec/spec.opts +1 -0
  36. data/spec/spec_helper.rb +11 -0
  37. data/spec/units/abstract_tool_spec.rb +112 -0
  38. data/spec/units/ffmpeg_spec.rb +703 -0
  39. data/spec/units/flvtool2_spec.rb +314 -0
  40. data/spec/units/inspector_spec.rb +41 -0
  41. data/spec/units/transcoder_spec.rb +140 -0
  42. data/website/index.html +219 -0
  43. data/website/index.txt +142 -0
  44. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  45. metadata +94 -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::Transcoder.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,9 @@
1
+ function toggle(obj) {
2
+ var el = document.getElementById(obj);
3
+ if ( el.style.display != 'none' ) {
4
+ el.style.display = 'none';
5
+ }
6
+ else {
7
+ el.style.display = '';
8
+ }
9
+ }
@@ -0,0 +1,79 @@
1
+ module RVideo # :nodoc:
2
+ module Tools # :nodoc:
3
+ class AbstractTool
4
+
5
+ #
6
+ # AbstractTool is an interface to every transcoder tool class (e.g.
7
+ # ffmpeg, flvtool2). Called by the Transcoder class.
8
+ #
9
+
10
+ def self.assign(cmd, options = {})
11
+ tool_name = cmd.split(" ").first
12
+ tool = "RVideo::Tools::#{tool_name.classify}".constantize.send(:new, cmd, options)
13
+ end
14
+
15
+
16
+ module InstanceMethods
17
+ attr_reader :options, :command, :raw_result
18
+
19
+ def initialize(raw_command, options = {})
20
+ @options = HashWithIndifferentAccess.new(options)
21
+ @command = interpolate_variables(raw_command)
22
+ end
23
+
24
+ #
25
+ # Look for variables surrounded by $, and interpolate with either (1)
26
+ # variables passed in the options hash, or special methods provided by
27
+ # the tool class (e.g. "$original_fps$" with ffmpeg).
28
+ #
29
+ # $foo$ should match
30
+ # \$foo or $foo\$ or \$foo\$ should not
31
+
32
+ def interpolate_variables(raw_command)
33
+
34
+ raw_command.scan(/[^\\]\$[-_a-zA-Z]+\$/).each do |match|
35
+ match.strip!
36
+ raw_command.gsub!(match, matched_variable(match))
37
+ end
38
+ raw_command.gsub("\\$", "$")
39
+ end
40
+
41
+ #
42
+ # Strip the $s. First, look for a supplied option that matches the
43
+ # variable name. If one is not found, look for a method that matches.
44
+ # If not found, raise ParameterError exception.
45
+ #
46
+
47
+ def matched_variable(match)
48
+ variable_name = match.gsub("$","")
49
+ if @options.key?(variable_name)
50
+ @options[variable_name] || ""
51
+ elsif self.respond_to? variable_name
52
+ self.send(variable_name)
53
+ else
54
+ raise TranscoderError::ParameterError, "recipe is expecting a value for the #{variable_name} parameter, but it was not provided."
55
+ end
56
+ end
57
+
58
+ #
59
+ # Execute the command and parse the result.
60
+ #
61
+
62
+ def execute
63
+ final_command = "#{@command} 2>&1"
64
+ Transcoder.logger.info("Executing: #{final_command}")
65
+ @raw_result = `#{final_command}`
66
+ Transcoder.logger.info("Result: #{@raw_result}")
67
+ parse_result(@raw_result)
68
+ end
69
+
70
+ private
71
+
72
+ def inspect_original
73
+ @original = Inspector.new(:file => options[:input_file])
74
+ end
75
+ end
76
+
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,111 @@
1
+ module RVideo
2
+ module Tools
3
+ class Ffmpeg
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :frame, :q, :size, :time, :bitrate, :video_size, :audio_size, :header_size, :overhead, :psnr, :fps
7
+
8
+ # Not sure if this is needed anymore...
9
+ def tool_command
10
+ 'ffmpeg'
11
+ end
12
+
13
+ #
14
+ # Return -r and the frame rate of the original file. E.g.:
15
+ #
16
+ # -r 29.97
17
+ #
18
+ # If the original frame rate can't be determined, return an empty
19
+ # string.
20
+ def original_fps
21
+ inspect_original if @original.nil?
22
+ if @original.fps
23
+ "-r #{@original.fps}"
24
+ else
25
+ ""
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def parse_result(result)
32
+ if m = /Unable for find a suitable output format for.*$/.match(result)
33
+ raise TranscoderError::InvalidCommand, m[0]
34
+ end
35
+
36
+ if m = /I\/O error occured\n(.*)$/.match(result)
37
+ raise TranscoderError::InvalidFile, "I/O error: #{m[1].strip}"
38
+ end
39
+
40
+ if m = /\n(.*)Unknown Format$/.match(result)
41
+ raise TranscoderError::InvalidFile, "unknown format (#{m[1]})"
42
+ end
43
+
44
+ if m = /\nERROR.*/m.match(result)
45
+ raise TranscoderError::InvalidFile, m[0]
46
+ end
47
+
48
+ if result =~ /usage: ffmpeg/
49
+ raise TranscoderError::InvalidCommand, "must pass a command to ffmpeg"
50
+ end
51
+
52
+ # Could not open './spec/../config/../tmp/processed/1/kites-1.avi'
53
+ if result =~ /Could not open .#{@output_file}.\Z/
54
+ raise TranscoderError, "Could not write output file to #{@output_file}"
55
+ end
56
+
57
+ full_details = /Press .* to stop encoding\n(.*)/m.match(result)
58
+ raise TranscoderError, "Unexpected result details (#{result})" if full_details.nil?
59
+ details = full_details[1].strip.gsub(/\s*\n\s*/," - ")
60
+
61
+ if details =~ /Could not write header/
62
+ raise TranscoderError, details
63
+ end
64
+
65
+ #frame= 584 q=6.0 Lsize= 708kB time=19.5 bitrate= 297.8kbits/s
66
+ #video:49kB audio:153kB global headers:0kB muxing overhead 250.444444%
67
+
68
+ #frame= 4126 q=31.0 Lsize= 5917kB time=69.1 bitrate= 702.0kbits/s
69
+ #video:2417kB audio:540kB global headers:0kB muxing overhead 100.140277%
70
+
71
+ #frame= 273 fps= 31 q=10.0 Lsize= 398kB time=5.9 bitrate= 551.8kbits/s
72
+ #video:284kB audio:92kB global headers:0kB muxing overhead 5.723981%
73
+
74
+ #mdb:94, lastbuf:0 skipping granule 0
75
+ #size= 1080kB time=69.1 bitrate= 128.0kbits /s
76
+ #video:0kB audio:1080kB global headers:0kB muxing overhead 0.002893%
77
+
78
+ # NOTE: had to remove "\s" from "\s.*L.*size=" from this regexp below. Not sure why.
79
+ # Unit tests were succeeding, but hand tests weren't.
80
+ if details =~ /video:/
81
+ #success = /^frame=\s*(\S*)\s*q=(\S*).*L.*size=\s*(\S*)\s*time=\s*(\S*)\s*bitrate=\s*(\S*)\s*/m.match(details)
82
+ @frame = sanitary_match(/frame=\s*(\S*)/, details)
83
+ @fps = sanitary_match(/fps=\s*(\S*)/, details)
84
+ @q = sanitary_match(/\s+q=\s*(\S*)/, details)
85
+ @size = sanitary_match(/size=\s*(\S*)/, details)
86
+ @time = sanitary_match(/time=\s*(\S*)/, details)
87
+ @bitrate = sanitary_match(/bitrate=\s*(\S*)/, details)
88
+
89
+ @video_size = /video:\s*(\S*)/.match(details)[1]
90
+ @audio_size = /audio:\s*(\S*)/.match(details)[1]
91
+ @header_size = /headers:\s*(\S*)/.match(details)[1]
92
+ @overhead = /overhead[:]*\s*(\S*)/.match(details)[1]
93
+ psnr_match = /PSNR=(.*)\s*size=/.match(details)
94
+ @psnr = psnr_match[1].strip if psnr_match
95
+ return true
96
+ end
97
+
98
+ #[mp3 @ 0x54340c]flv doesnt support that sample rate, choose from (44100, 22050, 11025)
99
+ #Could not write header for output file #0 (incorrect codec parameters ?)
100
+
101
+ raise TranscoderError::UnexpectedResult, details
102
+ end
103
+
104
+ def sanitary_match(regexp, string)
105
+ match = regexp.match(string)
106
+ return match[1] if match
107
+ end
108
+
109
+ end
110
+ end
111
+ end