debprado-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/License.txt +20 -0
- data/README.txt +91 -0
- data/RULES +11 -0
- data/debprado-rvideo.gemspec +10 -0
- data/lib/rvideo.rb +22 -0
- data/lib/rvideo/errors.rb +24 -0
- data/lib/rvideo/float.rb +7 -0
- data/lib/rvideo/inspector.rb +545 -0
- data/lib/rvideo/tools/abstract_tool.rb +339 -0
- data/lib/rvideo/tools/ffmpeg.rb +208 -0
- data/lib/rvideo/tools/ffmpeg2theora.rb +42 -0
- data/lib/rvideo/tools/flvtool2.rb +50 -0
- data/lib/rvideo/tools/mencoder.rb +106 -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/yamdi.rb +44 -0
- data/lib/rvideo/transcoder.rb +138 -0
- data/lib/rvideo/version.rb +9 -0
- metadata +74 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module RVideo
|
2
|
+
module Tools
|
3
|
+
class Ffmpeg2theora
|
4
|
+
include AbstractTool::InstanceMethods
|
5
|
+
|
6
|
+
attr_reader :raw_metadata
|
7
|
+
|
8
|
+
def tool_command
|
9
|
+
'ffmpeg2theora'
|
10
|
+
end
|
11
|
+
|
12
|
+
def format_video_quality(params={})
|
13
|
+
bitrate = params[:video_bit_rate].blank? ? nil : params[:video_bit_rate]
|
14
|
+
factor = (params[:scale][:width].to_f * params[:scale][:height].to_f * params[:fps].to_f)
|
15
|
+
case params[:video_quality]
|
16
|
+
when 'low'
|
17
|
+
" -v 1 "
|
18
|
+
when 'medium'
|
19
|
+
"-v 5 "
|
20
|
+
when 'high'
|
21
|
+
"-v 10 "
|
22
|
+
else
|
23
|
+
""
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse_result(result)
|
28
|
+
if m = /does not exist or has an unknown data format/.match(result)
|
29
|
+
raise TranscoderError::InvalidFile, "I/O error"
|
30
|
+
end
|
31
|
+
|
32
|
+
if m = /General output options/.match(result)
|
33
|
+
raise TranscoderError::InvalidCommand, "no command passed to ffmpeg2theora, or no output file specified"
|
34
|
+
end
|
35
|
+
|
36
|
+
@raw_metadata = result.empty? ? "No Results" : result
|
37
|
+
return true
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Warning: If you're dealing with large files, you should consider using yamdi instead.
|
2
|
+
module RVideo
|
3
|
+
module Tools
|
4
|
+
class Flvtool2
|
5
|
+
include AbstractTool::InstanceMethods
|
6
|
+
|
7
|
+
attr_reader :raw_metadata
|
8
|
+
|
9
|
+
#attr_reader :has_key_frames, :cue_points, :audiodatarate, :has_video, :stereo, :can_seek_to_end, :framerate, :audiosamplerate, :videocodecid, :datasize, :lasttimestamp,
|
10
|
+
# :audiosamplesize, :audiosize, :has_audio, :audiodelay, :videosize, :metadatadate, :metadatacreator, :lastkeyframetimestamp, :height, :filesize, :has_metadata, :audiocodecid,
|
11
|
+
# :duration, :videodatarate, :has_cue_points, :width
|
12
|
+
|
13
|
+
def tool_command
|
14
|
+
'flvtool2'
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_result(result)
|
20
|
+
if result.empty?
|
21
|
+
return true
|
22
|
+
end
|
23
|
+
|
24
|
+
if m = /ERROR: No such file or directory(.*)\n/.match(result)
|
25
|
+
raise TranscoderError::InputFileNotFound, m[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
if m = /ERROR: IO is not a FLV stream/.match(result)
|
29
|
+
raise TranscoderError::InvalidFile, "input must be a valid FLV file"
|
30
|
+
end
|
31
|
+
|
32
|
+
if m = /Copyright.*Norman Timmler/i.match(result)
|
33
|
+
raise TranscoderError::InvalidCommand, "command printed flvtool2 help text (and presumably didn't execute)"
|
34
|
+
end
|
35
|
+
|
36
|
+
if m = /ERROR: undefined method .?timestamp.? for nil/.match(result)
|
37
|
+
raise TranscoderError::InvalidFile, "Output file was empty (presumably)"
|
38
|
+
end
|
39
|
+
|
40
|
+
if m = /\A---(.*)...\Z/m.match(result)
|
41
|
+
@raw_metadata = m[0]
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
45
|
+
raise TranscoderError::UnexpectedResult, result
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,106 @@
|
|
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
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def parse_result(result)
|
59
|
+
if m = /Exiting.*No output file specified/.match(result)
|
60
|
+
raise TranscoderError::InvalidCommand, "no command passed to mencoder, or no output file specified"
|
61
|
+
end
|
62
|
+
|
63
|
+
if m = /counldn't set specified parameters, exiting/.match(result)
|
64
|
+
raise TranscoderError::InvalidCommand, "a combination of the recipe parameters is invalid: #{result}"
|
65
|
+
end
|
66
|
+
|
67
|
+
if m = /Sorry, this file format is not recognized\/supported/.match(result)
|
68
|
+
raise TranscoderError::InvalidFile, "unknown format"
|
69
|
+
end
|
70
|
+
|
71
|
+
if m = /Cannot open file\/device./.match(result)
|
72
|
+
raise TranscoderError::InvalidFile, "I/O error"
|
73
|
+
end
|
74
|
+
|
75
|
+
if m = /File not found:$/.match(result)
|
76
|
+
raise TranscoderError::InvalidFile, "I/O error"
|
77
|
+
end
|
78
|
+
|
79
|
+
video_details = result.match /Video stream:(.*)$/
|
80
|
+
if video_details
|
81
|
+
@bitrate = sanitary_match(/Video stream:\s*([0-9.]*)/, video_details[0])
|
82
|
+
@video_size = sanitary_match(/size:\s*(\d*)\s*(\S*)/, video_details[0])
|
83
|
+
@time = sanitary_match(/bytes\s*([0-9.]*)/, video_details[0])
|
84
|
+
@frame = sanitary_match(/secs\s*(\d*)/, video_details[0])
|
85
|
+
@output_fps = (@frame.to_f / @time.to_f).round_to(3)
|
86
|
+
elsif result =~ /Video stream is mandatory/
|
87
|
+
raise TranscoderError::InvalidFile, "Video stream required, and no video stream found"
|
88
|
+
end
|
89
|
+
|
90
|
+
audio_details = result.match /Audio stream:(.*)$/
|
91
|
+
if audio_details
|
92
|
+
@audio_size = sanitary_match(/size:\s*(\d*)\s*(\S*)/, audio_details[0])
|
93
|
+
else
|
94
|
+
@audio_size = 0
|
95
|
+
end
|
96
|
+
@size = (@video_size.to_i + @audio_size.to_i).to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
def sanitary_match(regexp, string)
|
100
|
+
match = regexp.match(string)
|
101
|
+
return match[1] if match
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
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,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,138 @@
|
|
1
|
+
module RVideo # :nodoc:
|
2
|
+
class Transcoder
|
3
|
+
|
4
|
+
attr_reader :executed_commands, :processed, :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
|
+
#
|
50
|
+
# Configure logging. Pass a valid Ruby logger object.
|
51
|
+
#
|
52
|
+
# logger = Logger.new(STDOUT)
|
53
|
+
# RVideo::Transcoder.logger = logger
|
54
|
+
#
|
55
|
+
|
56
|
+
def self.logger=(l)
|
57
|
+
@logger = l
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.logger
|
61
|
+
if @logger.nil?
|
62
|
+
@logger = Logger.new('/dev/null')
|
63
|
+
end
|
64
|
+
|
65
|
+
@logger
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Requires a command and a hash of various interpolated options. The
|
70
|
+
# command should be one or more lines of transcoder tool commands (e.g.
|
71
|
+
# ffmpeg, flvtool2). Interpolate options by adding $option_key$ to the
|
72
|
+
# recipe, and passing :option_key => "value" in the options hash.
|
73
|
+
#
|
74
|
+
# recipe = "ffmpeg -i $input_file$ -ar 22050 -ab 64 -f flv -r 29.97
|
75
|
+
# recipe += "-s $resolution$ -y $output_file$"
|
76
|
+
# recipe += "\nflvtool2 -U $output_file$"
|
77
|
+
#
|
78
|
+
# transcoder = RVideo::Transcoder.new("/path/to/input.mov")
|
79
|
+
# begin
|
80
|
+
# transcoder.execute(recipe, {:output_file => "/path/to/output.flv", :resolution => "320x240"})
|
81
|
+
# rescue TranscoderError => e
|
82
|
+
# puts "Unable to transcode file: #{e.class} - #{e.message}"
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
|
86
|
+
def execute(task, options = {})
|
87
|
+
t1 = Time.now
|
88
|
+
|
89
|
+
if @input_file.nil?
|
90
|
+
@input_file = options[:input_file]
|
91
|
+
end
|
92
|
+
|
93
|
+
Transcoder.logger.info("\nNew transcoder job\n================\nTask: #{task}\nOptions: #{options.inspect}")
|
94
|
+
parse_and_execute(task, options)
|
95
|
+
@processed = Inspector.new(:file => options[:output_file])
|
96
|
+
result = check_integrity
|
97
|
+
Transcoder.logger.info("\nFinished task. Total errors: #{@errors.size}\n")
|
98
|
+
@total_time = Time.now - t1
|
99
|
+
result
|
100
|
+
rescue TranscoderError => e
|
101
|
+
raise e
|
102
|
+
rescue Exception => e
|
103
|
+
Transcoder.logger.error("[ERROR] Unhandled RVideo exception: #{e.class} - #{e.message}\n#{e.backtrace}")
|
104
|
+
raise TranscoderError::UnknownError, "Unexpected RVideo error: #{e.message} (#{e.class})"
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def check_input_file(input_file)
|
110
|
+
if input_file and !FileTest.exist?(input_file.gsub("\"",""))
|
111
|
+
raise TranscoderError::InputFileNotFound, "File not found (#{input_file})"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def check_integrity
|
116
|
+
precision = 1.1
|
117
|
+
if @processed.invalid?
|
118
|
+
@errors << "Output file invalid"
|
119
|
+
elsif (@processed.duration >= (original.duration * precision) or @processed.duration <= (original.duration / precision))
|
120
|
+
@errors << "Original file has a duration of #{original.duration}, but processed file has a duration of #{@processed.duration}"
|
121
|
+
end
|
122
|
+
return @errors.size == 0
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_and_execute(task, options = {})
|
126
|
+
raise TranscoderError::ParameterError, "Expected a recipe class (as a string), but got a #{task.class.to_s} (#{task})" unless task.is_a? String
|
127
|
+
options = options.merge(:input_file => @input_file)
|
128
|
+
|
129
|
+
commands = task.split("\n").compact
|
130
|
+
commands.each do |c|
|
131
|
+
tool = Tools::AbstractTool.assign(c, options)
|
132
|
+
tool.original = @original
|
133
|
+
tool.execute
|
134
|
+
executed_commands << tool
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|