ez_video 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +41 -0
- data/LICENSE.txt +20 -0
- data/README.md +132 -0
- data/ez_video.gemspec +28 -0
- data/lib/ez_video.rb +15 -0
- data/lib/rvideo/errors.rb +27 -0
- data/lib/rvideo/float.rb +7 -0
- data/lib/rvideo/inspector.rb +573 -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 +65 -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})", e.backtrace
|
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
|