stlondemand-rvideo 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/CHANGELOG +70 -0
  2. data/ENV +100 -0
  3. data/ENV2 +129 -0
  4. data/LICENSE +20 -0
  5. data/Manifest +72 -0
  6. data/README +106 -0
  7. data/RULES +11 -0
  8. data/Rakefile +63 -0
  9. data/config/boot.rb +25 -0
  10. data/lib/rvideo.rb +49 -0
  11. data/lib/rvideo/command_executor.rb +91 -0
  12. data/lib/rvideo/errors.rb +24 -0
  13. data/lib/rvideo/float.rb +7 -0
  14. data/lib/rvideo/frame_capturer.rb +139 -0
  15. data/lib/rvideo/inspector.rb +519 -0
  16. data/lib/rvideo/reporter.rb +176 -0
  17. data/lib/rvideo/reporter/views/index.html.erb +27 -0
  18. data/lib/rvideo/reporter/views/report.css +27 -0
  19. data/lib/rvideo/reporter/views/report.html.erb +81 -0
  20. data/lib/rvideo/reporter/views/report.js +9 -0
  21. data/lib/rvideo/string.rb +5 -0
  22. data/lib/rvideo/tools/abstract_tool.rb +459 -0
  23. data/lib/rvideo/tools/ffmpeg.rb +314 -0
  24. data/lib/rvideo/tools/ffmpeg2theora.rb +72 -0
  25. data/lib/rvideo/tools/flvtool2.rb +50 -0
  26. data/lib/rvideo/tools/handbrakecli.rb +61 -0
  27. data/lib/rvideo/tools/lame.rb +58 -0
  28. data/lib/rvideo/tools/mencoder.rb +126 -0
  29. data/lib/rvideo/tools/mp4box.rb +21 -0
  30. data/lib/rvideo/tools/mp4creator.rb +35 -0
  31. data/lib/rvideo/tools/mplayer.rb +31 -0
  32. data/lib/rvideo/tools/qtfaststart.rb +37 -0
  33. data/lib/rvideo/tools/segmenter.rb +29 -0
  34. data/lib/rvideo/tools/yamdi.rb +44 -0
  35. data/lib/rvideo/transcoder.rb +170 -0
  36. data/lib/rvideo/version.rb +9 -0
  37. data/scripts/txt2html +67 -0
  38. data/spec/files/boat.avi +0 -0
  39. data/spec/files/kites.mp4 +0 -0
  40. data/spec/fixtures/ffmpeg_builds.yml +28 -0
  41. data/spec/fixtures/ffmpeg_results.yml +608 -0
  42. data/spec/fixtures/files.yml +398 -0
  43. data/spec/fixtures/recipes.yml +58 -0
  44. data/spec/integrations/formats_spec.rb +315 -0
  45. data/spec/integrations/frame_capturer_spec.rb +26 -0
  46. data/spec/integrations/inspection_spec.rb +125 -0
  47. data/spec/integrations/recipes_spec.rb +0 -0
  48. data/spec/integrations/rvideo_spec.rb +17 -0
  49. data/spec/integrations/transcoder_integration_spec.rb +29 -0
  50. data/spec/integrations/transcoding_spec.rb +9 -0
  51. data/spec/spec.opts +1 -0
  52. data/spec/spec_helper.rb +16 -0
  53. data/spec/support.rb +36 -0
  54. data/spec/units/abstract_tool_spec.rb +111 -0
  55. data/spec/units/command_executor_spec.rb +106 -0
  56. data/spec/units/ffmpeg_spec.rb +385 -0
  57. data/spec/units/flvtool2_spec.rb +323 -0
  58. data/spec/units/frame_capturer_spec.rb +71 -0
  59. data/spec/units/inspector_spec.rb +59 -0
  60. data/spec/units/mencoder_spec.rb +4994 -0
  61. data/spec/units/mp4box_spec.rb +34 -0
  62. data/spec/units/mp4creator_spec.rb +34 -0
  63. data/spec/units/mplayer_spec.rb +34 -0
  64. data/spec/units/qtfaststart_spec.rb +35 -0
  65. data/spec/units/string_spec.rb +8 -0
  66. data/spec/units/transcoder_spec.rb +154 -0
  67. data/stlondemand-rvideo.gemspec +36 -0
  68. data/tasks/deployment.rake +5 -0
  69. data/tasks/testing.rake +27 -0
  70. data/tasks/transcoding.rake +40 -0
  71. data/tasks/website.rake +8 -0
  72. data/test_progress_reporting.rb +14 -0
  73. metadata +187 -0
@@ -0,0 +1,58 @@
1
+ module RVideo
2
+ module Tools
3
+ class Lame
4
+ include AbstractTool::InstanceMethods
5
+ attr_reader :raw_metadata
6
+
7
+ def tool_command
8
+ 'lame'
9
+ end
10
+
11
+
12
+ def execute_with_progress(&block)
13
+ RVideo.logger.info("\nExecuting Command: #{@command}\n")
14
+ do_execute_with_progress(@command, &block)
15
+ rescue RVideo::CommandExecutor::ProcessHungError
16
+ raise TranscoderError, "Transcoder hung."
17
+ end
18
+
19
+ def do_execute_with_progress(command,&block)
20
+ @raw_result = ''
21
+ CommandExecutor::execute_with_block(command, "\r") do |line|
22
+ progress = parse_progress(line)
23
+ block.call(progress) if block && progress
24
+ @raw_result += line + "\r"
25
+ end
26
+ end
27
+
28
+ def parse_progress(line)
29
+ if line =~ /\((\d+)\%\)/
30
+ $1.to_i > 100 ? 100 : $1.to_i
31
+ else
32
+ nil
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_result(result)
39
+
40
+ if m = /usage: lame [options] <infile> [outfile]/.match(result)
41
+ raise TranscoderError::InvalidCommand, "usage: lame [options] <infile> [outfile]"
42
+ end
43
+
44
+ if m = /Warning: unsupported audio format/.match(result)
45
+ raise TranscoderError::InvalidFile, "Warning: unsupported audio format"
46
+ end
47
+
48
+ if m = /Could not find/.match(result)
49
+ raise TranscoderError::InvalidFile, "No such file or directory"
50
+ end
51
+
52
+ @raw_metadata = result.empty? ? "No Results" : result
53
+ return true
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,126 @@
1
+ module RVideo
2
+ module Tools
3
+ class Mencoder
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :frame, :size, :time, :bitrate, :video_size, :audio_size, :output_fps
7
+
8
+ def tool_command
9
+ 'mencoder'
10
+ end
11
+
12
+ def format_fps(params={})
13
+ " -ofps #{params[:fps]}"
14
+ end
15
+
16
+ def format_resolution(params={})
17
+ p = " -vf scale=#{params[:scale][:width]}:#{params[:scale][:height]}"
18
+ if params[:letterbox]
19
+ p += ",expand=#{params[:letterbox][:width]}:#{params[:letterbox][:height]}"
20
+ end
21
+ p += ",harddup"
22
+ end
23
+
24
+ def format_audio_channels(params={})
25
+ " -channels #{params[:channels]}"
26
+ end
27
+
28
+ def format_audio_bit_rate(params={})
29
+ " br=#{params[:bit_rate]}:"
30
+ end
31
+
32
+ def format_audio_sample_rate(params={})
33
+ " -srate #{params[:sample_rate]}"
34
+ end
35
+
36
+ def format_video_quality(params={})
37
+ bitrate = params[:video_bit_rate].blank? ? nil : params[:video_bit_rate]
38
+ factor = (params[:scale][:width].to_f * params[:scale][:height].to_f * params[:fps].to_f)
39
+ case params[:video_quality]
40
+ when 'low'
41
+ bitrate ||= (factor / 12000).to_i
42
+ " -x264encopts threads=auto:subq=1:me=dia:frameref=1:crf=30:bitrate=#{bitrate} "
43
+ when 'medium'
44
+ bitrate ||= (factor / 9000).to_i
45
+ " -x264encopts threads=auto:subq=3:me=hex:frameref=2:crf=22:bitrate=#{bitrate} "
46
+ when 'high'
47
+ bitrate ||= (factor / 3600).to_i
48
+ " -x264encopts threads=auto:subq=6:me=dia:frameref=3:crf=18:bitrate=#{bitrate} "
49
+ else
50
+ ""
51
+ end
52
+ end
53
+
54
+ def execute_with_progress(&block)
55
+ RVideo.logger.info("\nExecuting Command: #{@command}\n")
56
+ do_execute_with_progress(@command, &block)
57
+ rescue RVideo::CommandExecutor::ProcessHungError
58
+ raise TranscoderError, "Transcoder hung."
59
+ end
60
+
61
+ def do_execute_with_progress(command,&block)
62
+ @raw_result = ''
63
+ stderr_result, stdout_result = CommandExecutor::execute_with_block(command, "\r", false) do |line|
64
+ progress = parse_progress(line)
65
+ block.call(progress) if block && progress
66
+ @raw_result += line + "\r"
67
+ end
68
+ @raw_result += stderr_result
69
+ end
70
+
71
+ def parse_progress(line)
72
+ if line =~ /Pos:[^(]*\((\d+)\%\)/
73
+ $1.to_i > 100 ? 100 : $1.to_i
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def parse_result(result)
82
+ if m = /Exiting.*No output file specified/.match(result)
83
+ raise TranscoderError::InvalidCommand, "no command passed to mencoder, or no output file specified"
84
+ end
85
+
86
+ if m = /counldn't set specified parameters, exiting/.match(result)
87
+ raise TranscoderError::InvalidCommand, "a combination of the recipe parameters is invalid: #{result}"
88
+ end
89
+
90
+ if m = /Sorry, this file format is not recognized\/supported/.match(result)
91
+ raise TranscoderError::InvalidFile, "unknown format"
92
+ end
93
+
94
+ if m = /Cannot open file\/device./.match(result)
95
+ raise TranscoderError::InvalidFile, "I/O error"
96
+ end
97
+
98
+ if m = /File not found:$/.match(result)
99
+ raise TranscoderError::InvalidFile, "I/O error"
100
+ end
101
+
102
+ video_details = result.match /Video stream:(.*)$/
103
+ if video_details
104
+ @bitrate = video_details[0][/Video stream:\s*([0-9.]*)/, 1]
105
+ @video_size = video_details[0][/size:\s*(\d*)\s*(\S*)/, 1]
106
+ @time = video_details[0][/bytes\s*([0-9.]*)/, 1]
107
+ @frame = video_details[0][/secs\s*(\d*)/, 1]
108
+ @output_fps = (@frame.to_f / @time.to_f).round_to(3)
109
+
110
+ elsif result =~ /Video stream is mandatory/
111
+ raise TranscoderError::InvalidFile,
112
+ "Video stream required, and no video stream found"
113
+ end
114
+
115
+ audio_details = result.match /Audio stream:(.*)$/
116
+ if audio_details
117
+ @audio_size = audio_details[0][/size:\s*(\d*)\s*\S*/, 1]
118
+ else
119
+ @audio_size = 0
120
+ end
121
+ @size = (@video_size.to_i + @audio_size.to_i).to_s
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,21 @@
1
+ module RVideo
2
+ module Tools
3
+ class Mp4box
4
+ include AbstractTool::InstanceMethods
5
+ attr_reader :raw_metadata
6
+
7
+ def tool_command
8
+ 'MP4Box'
9
+ end
10
+
11
+ private
12
+
13
+ def parse_result(result)
14
+ #currently, no useful info returned in result to determine if successful or not
15
+ @raw_metadata = result.empty? ? "No Results" : result
16
+ return true
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ module RVideo
2
+ module Tools
3
+ class Mp4creator
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :raw_metadata
7
+
8
+ def tool_command
9
+ 'mp4creator'
10
+ end
11
+
12
+ def format_fps(params={})
13
+ " -rate=#{params[:fps]}"
14
+ end
15
+
16
+ def parse_result(result)
17
+ if m = /can't open file/.match(result)
18
+ raise TranscoderError::InvalidFile, "I/O error"
19
+ end
20
+
21
+ if m = /unknown file type/.match(result)
22
+ raise TranscoderError::InvalidFile, "I/O error"
23
+ end
24
+
25
+ if @options['output_file'] && !File.exist?(@options['output_file'])
26
+ raise TranscoderError::UnexpectedResult, "An unknown error has occured with mp4creator:#{result}"
27
+ end
28
+
29
+ @raw_metadata = result.empty? ? "No Results" : result
30
+ return true
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module RVideo
2
+ module Tools
3
+ class Mplayer
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :raw_metadata
7
+
8
+ def tool_command
9
+ 'mplayer'
10
+ end
11
+
12
+ def parse_result(result)
13
+ if m = /This will likely crash/.match(result)
14
+ raise TranscoderError::InvalidFile, "unknown format"
15
+ end
16
+
17
+ if m = /Failed to open/.match(result)
18
+ raise TranscoderError::InvalidFile, "I/O error"
19
+ end
20
+
21
+ if m = /File not found/.match(result)
22
+ raise TranscoderError::InvalidFile, "I/O error"
23
+ end
24
+
25
+ @raw_metadata = result.empty? ? "No Results" : result
26
+ return true
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ module RVideo
2
+ module Tools
3
+ class QtFaststart
4
+ include AbstractTool::InstanceMethods
5
+ attr_reader :raw_metadata
6
+
7
+ def tool_command
8
+ 'qt-faststart'
9
+ end
10
+
11
+ private
12
+
13
+ def parse_result(result)
14
+
15
+ if m = /Usage: qt-faststart <infile.mov> <outfile.mov>/.match(result)
16
+ raise TranscoderError::InvalidCommand, "Usage: qt-faststart <infile.mov> <outfile.mov>"
17
+ end
18
+
19
+ if m = /last atom in file was not a moov atom/.match(result)
20
+ raise TranscoderError::InvalidFile, "Could not find moov atom"
21
+ end
22
+
23
+ if m = /No such file or directory/.match(result)
24
+ raise TranscoderError::InvalidFile, "No such file or directory"
25
+ end
26
+
27
+ if m = /Undefined error:/.match(result)
28
+ raise TranscoderError::UnexpectedResult, "Undefined error"
29
+ end
30
+
31
+ @raw_metadata = result.empty? ? "No Results" : result
32
+ return true
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ module RVideo
2
+ module Tools
3
+ class Segmenter
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :raw_metadata
7
+
8
+ def tool_command
9
+ 'segmenter'
10
+ end
11
+
12
+ private
13
+
14
+ def parse_result(result)
15
+ if m = /Could not write mpegts header to first output file/.match(result)
16
+ raise TranscoderError::InvalidFile
17
+ end
18
+
19
+ if m = /Usage: segmenter <input MPEG-TS file> <segment duration in seconds> <output MPEG-TS file prefix> <output m3u8 index file> <http prefix>/.match(result)
20
+ raise TranscoderError::InvalidCommand, "Usage: segmenter <input MPEG-TS file> <segment duration in seconds> <output MPEG-TS file prefix> <output m3u8 index file> <http prefix>"
21
+ end
22
+
23
+ # TODO: parse segmenter results
24
+ return true
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,44 @@
1
+ module RVideo
2
+ module Tools
3
+ class Yamdi
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :raw_metadata
7
+
8
+ def tool_command
9
+ 'yamdi'
10
+ end
11
+
12
+ private
13
+
14
+ def parse_result(result)
15
+ if result.empty?
16
+ return true
17
+ end
18
+
19
+ if m = /Couldn't stat on (.*)/.match(result)
20
+ raise TranscoderError::InputFileNotFound, m[0]
21
+ end
22
+
23
+ if m = /The input file is not a FLV./.match(result)
24
+ raise TranscoderError::InvalidFile, "input must be a valid FLV file"
25
+ end
26
+
27
+ if m = /\(c\) \d{4} Ingo Oppermann/i.match(result)
28
+ raise TranscoderError::InvalidCommand, "command printed yamdi help text (and presumably didn't execute)"
29
+ end
30
+
31
+ if m = /Please provide at least one output file/i.match(result)
32
+ raise TranscoderError::InvalidCommand, "command did not contain a valid output file. Yamdi expects a -o switch."
33
+ end
34
+
35
+ if m = /ERROR: undefined method .?timestamp.? for nil/.match(result)
36
+ raise TranscoderError::InvalidFile, "Output file was empty (presumably)"
37
+ end
38
+
39
+ raise TranscoderError::UnexpectedResult, result
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,170 @@
1
+ module RVideo # :nodoc:
2
+ class Transcoder
3
+
4
+ attr_reader :executed_commands, :errors, :warnings, :total_time
5
+
6
+ #
7
+ # To transcode a video, initialize a Transcoder object:
8
+ #
9
+ # transcoder = RVideo::Transcoder.new("/path/to/input.mov")
10
+ #
11
+ # Then pass a recipe and valid options to the execute method
12
+ #
13
+ # recipe = "ffmpeg -i $input_file$ -ar 22050 -ab 64 -f flv -r 29.97 -s"
14
+ # recipe += " $resolution$ -y $output_file$"
15
+ # recipe += "\nflvtool2 -U $output_file$"
16
+ # begin
17
+ # transcoder.execute(recipe, {:output_file => "/path/to/output.flv",
18
+ # :resolution => "640x360"})
19
+ # rescue TranscoderError => e
20
+ # puts "Unable to transcode file: #{e.class} - #{e.message}"
21
+ # end
22
+ #
23
+ # If the job succeeds, you can access the metadata of the input and output
24
+ # files with:
25
+ #
26
+ # transcoder.original # RVideo::Inspector object
27
+ # transcoder.processed # RVideo::Inspector object
28
+ #
29
+ # If the transcoding succeeds, the file may still have problems. RVideo
30
+ # will populate an errors array if the duration of the processed video
31
+ # differs from the duration of the original video, or if the processed
32
+ # file is unreadable.
33
+ #
34
+
35
+ def initialize(input_file = nil)
36
+ # Allow a nil input_file for backwards compatibility. (Change at 1.0?)
37
+ check_input_file(input_file)
38
+
39
+ @input_file = input_file
40
+ @executed_commands = []
41
+ @errors = []
42
+ @warnings = []
43
+ end
44
+
45
+ def original
46
+ @original ||= Inspector.new(:file => @input_file)
47
+ end
48
+
49
+ def processed
50
+ if @output_file
51
+ @processed ||= Inspector.new(:file => @output_file)
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+ #
58
+ # Requires a command and a hash of various interpolated options. The
59
+ # command should be one or more lines of transcoder tool commands (e.g.
60
+ # ffmpeg, flvtool2). Interpolate options by adding $option_key$ to the
61
+ # recipe, and passing :option_key => "value" in the options hash.
62
+ #
63
+ # recipe = "ffmpeg -i $input_file$ -ar 22050 -ab 64 -f flv -r 29.97
64
+ # recipe += "-s $resolution$ -y $output_file$"
65
+ # recipe += "\nflvtool2 -U $output_file$"
66
+ #
67
+ # transcoder = RVideo::Transcoder.new("/path/to/input.mov")
68
+ # begin
69
+ # transcoder.execute(recipe, {:output_file => "/path/to/output.flv", :resolution => "320x240"})
70
+ # rescue TranscoderError => e
71
+ # puts "Unable to transcode file: #{e.class} - #{e.message}"
72
+ # end
73
+ #
74
+
75
+ def execute(task, options = {})
76
+ t1 = Time.now
77
+
78
+ if @input_file.nil?
79
+ @input_file = options[:input_file]
80
+ end
81
+
82
+ RVideo.logger.info("\nNew transcoder job\n================\nTask: #{task}\nOptions: #{options.inspect}")
83
+
84
+ if block_given?
85
+ parse_and_execute(task, options) do |tool, progress|
86
+ yield(tool, progress)
87
+ end
88
+ else
89
+ parse_and_execute(task, options)
90
+ end
91
+
92
+ if @output_file.nil?
93
+ @output_file = options[:output_file]
94
+ end
95
+ result = check_integrity
96
+ RVideo.logger.info("\nFinished task. Total errors: #{@errors.size}\n")
97
+ @total_time = Time.now - t1
98
+ result
99
+ rescue TranscoderError => e
100
+ raise e
101
+ rescue Exception => e
102
+ handle_unknown_error(e)
103
+ end
104
+
105
+ private
106
+
107
+ def handle_unknown_error(e)
108
+ RVideo.logger.error "[ERROR] Unhandled RVideo exception: #{e.class} - #{e.message}"
109
+ RVideo.logger.error e.backtrace.join("\n\t")
110
+ raise TranscoderError::UnknownError, "Unexpected RVideo error: #{e.message} (#{e.class})"
111
+ end
112
+
113
+ def check_input_file(input_file)
114
+ if input_file and !FileTest.exist?(input_file.gsub("\"",""))
115
+ raise TranscoderError::InputFileNotFound, "File not found (#{input_file})"
116
+ end
117
+ end
118
+
119
+ def check_integrity
120
+ precision = 1.1
121
+ if processed.invalid?
122
+ @errors << "Output file invalid"
123
+ elsif processed.duration <= 0
124
+ @errors << "Processed file has a duration of #{processed.duration}"
125
+ end
126
+ return @errors.size == 0
127
+ end
128
+
129
+ def parse_and_execute(task, options = {})
130
+ raise TranscoderError::ParameterError, "Expected a recipe class (as a string), but got a #{task.class.to_s} (#{task})" unless task.is_a? String
131
+ options = options.merge(:input_file => @input_file)
132
+
133
+ commands = task.split(/[\n;]/).compact
134
+ commands_with_progress = number_of_tools_supporting_progress(commands)
135
+
136
+ progress_by_tools={}
137
+ commands.each do |c|
138
+ tool = Tools::AbstractTool.assign(c, options)
139
+ tool.original = original
140
+
141
+ if block_given?
142
+ tool.execute do |progress|
143
+ progress_by_tools[c.object_id] = progress
144
+ sum = progress_by_tools.values.inject(0) { |s,v| s += v }
145
+ if commands_with_progress > 0
146
+ total_progress = sum / commands_with_progress
147
+ yield(tool, total_progress) if total_progress != @prev_progress
148
+ @prev_progress = total_progress
149
+ end
150
+ end
151
+ else
152
+ tool.execute
153
+ end
154
+
155
+ executed_commands << tool
156
+ end
157
+ end
158
+
159
+
160
+ def number_of_tools_supporting_progress(commands)
161
+ n = 0
162
+ commands.each do |c|
163
+ tool_name = c.split(" ").first
164
+ n += 1 if RVideo::Tools.const_get(tool_name.underscore.classify).method_defined?(:execute_with_progress)
165
+ end
166
+ n
167
+ end
168
+
169
+ end
170
+ end