video_converter 0.6.2 → 0.6.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/video_converter.rb +2 -2
- data/lib/video_converter/base.rb +23 -34
- data/lib/video_converter/command.rb +1 -1
- data/lib/video_converter/ffmpeg.rb +126 -79
- data/lib/video_converter/input.rb +68 -84
- data/lib/video_converter/live_segmenter.rb +17 -24
- data/lib/video_converter/{hds.rb → mp4frag.rb} +8 -15
- data/lib/video_converter/output.rb +16 -80
- data/lib/video_converter/version.rb +1 -1
- data/test/video_converter_test.rb +28 -10
- metadata +5 -5
data/lib/video_converter.rb
CHANGED
@@ -4,13 +4,13 @@ require "shellwords"
|
|
4
4
|
require "video_converter/array"
|
5
5
|
require "video_converter/base"
|
6
6
|
require "video_converter/command"
|
7
|
-
require "video_converter/ffmpeg"
|
8
7
|
require "video_converter/faststart"
|
8
|
+
require "video_converter/ffmpeg"
|
9
9
|
require "video_converter/hash"
|
10
10
|
require "video_converter/hls"
|
11
|
-
require "video_converter/hds"
|
12
11
|
require "video_converter/input"
|
13
12
|
require "video_converter/live_segmenter"
|
13
|
+
require "video_converter/mp4frag"
|
14
14
|
require "video_converter/object"
|
15
15
|
require "video_converter/output"
|
16
16
|
require "video_converter/version"
|
data/lib/video_converter/base.rb
CHANGED
@@ -2,40 +2,28 @@
|
|
2
2
|
|
3
3
|
module VideoConverter
|
4
4
|
class Base
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :inputs, :outputs
|
6
6
|
|
7
7
|
def initialize params
|
8
|
-
self.
|
9
|
-
self.outputs = Array.wrap(params[:output] || params[:outputs]).map
|
10
|
-
Output.new(output.merge(:uid => uid))
|
11
|
-
end
|
12
|
-
self.inputs = Array.wrap(params[:input] || params[:inputs]).map do |input|
|
13
|
-
Input.new(input, outputs)
|
14
|
-
end
|
15
|
-
self.clear_tmp = params[:clear_tmp].nil? ? true : params[:clear_tmp]
|
8
|
+
self.inputs = Array.wrap(params[:input] || params[:inputs]).map { |input| Input.new(input) }
|
9
|
+
self.outputs = Array.wrap(params[:output] || params[:outputs]).map { |output| Output.new(output.merge(:uid => params[:uid] ? params[:uid].to_s : (Socket.gethostname + object_id.to_s))) }
|
16
10
|
end
|
17
11
|
|
18
12
|
def run
|
19
|
-
|
20
|
-
clear if clear_tmp && success
|
21
|
-
success
|
13
|
+
convert && faststart && make_screenshots && segment && clear
|
22
14
|
end
|
23
15
|
|
16
|
+
# XXX inject instead of each would be better
|
24
17
|
def convert
|
25
18
|
success = true
|
26
|
-
inputs.each
|
27
|
-
input.output_groups.each do |group|
|
28
|
-
success &&= Ffmpeg.new(input, group).run
|
29
|
-
end
|
30
|
-
end
|
19
|
+
inputs.each { |input| success &&= Ffmpeg.new(input, outputs).run }
|
31
20
|
success
|
32
21
|
end
|
33
22
|
|
23
|
+
# TODO use for faststart ffmpeg moveflags
|
34
24
|
def faststart
|
35
25
|
success = true
|
36
|
-
outputs.each
|
37
|
-
success &&= Faststart.new(output).run if output.faststart
|
38
|
-
end
|
26
|
+
outputs.each { |output| success &&= Faststart.new(output).run if output.faststart }
|
39
27
|
success
|
40
28
|
end
|
41
29
|
|
@@ -50,35 +38,36 @@ module VideoConverter
|
|
50
38
|
def segment
|
51
39
|
success = true
|
52
40
|
inputs.each do |input|
|
53
|
-
input.output_groups.each do |group|
|
41
|
+
input.output_groups(input.select_outputs(outputs)).each do |group|
|
54
42
|
if playlist = group.detect { |output| output.type == 'playlist' }
|
55
|
-
success &&= if playlist.
|
56
|
-
LiveSegmenter.
|
43
|
+
success &&= if File.extname(playlist.filename) == '.m3u8'
|
44
|
+
LiveSegmenter.run(input, group)
|
57
45
|
else
|
58
|
-
|
46
|
+
Mp4frag.run(input, group)
|
59
47
|
end
|
60
48
|
end
|
61
49
|
end
|
62
50
|
end
|
51
|
+
success
|
63
52
|
end
|
64
53
|
|
65
54
|
def split
|
66
|
-
Ffmpeg.
|
55
|
+
Ffmpeg.split(inputs.first, outputs.first)
|
67
56
|
end
|
68
57
|
|
69
58
|
def concat
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
success
|
59
|
+
Ffmpeg.concat(inputs, outputs.first)
|
60
|
+
end
|
61
|
+
|
62
|
+
def mux
|
63
|
+
Ffmpeg.mux(inputs, outputs.first)
|
76
64
|
end
|
77
65
|
|
78
66
|
def clear
|
79
|
-
|
80
|
-
outputs.map { |output| output.passlogfile }.uniq.compact.each { |passlogfile|
|
81
|
-
outputs.select { |output| output.type == 'segmented' }.each { |output|
|
67
|
+
Command.new("cat #{outputs.first.log} >> #{VideoConverter.log} && rm #{outputs.first.log}").execute
|
68
|
+
outputs.map { |output| output.options[:passlogfile] }.uniq.compact.each { |passlogfile| Command.new("rm #{passlogfile}*").execute }
|
69
|
+
outputs.select { |output| output.type == 'segmented' }.each { |output| Command.new("rm #{output.ffmpeg_output}").execute }
|
70
|
+
true
|
82
71
|
end
|
83
72
|
end
|
84
73
|
end
|
@@ -3,120 +3,167 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Ffmpeg
|
5
5
|
class << self
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :aliases, :bin, :ffprobe_bin
|
7
|
+
attr_accessor :one_pass_command, :first_pass_command, :second_pass_command, :keyframes_command, :split_command, :concat_command, :mux_command
|
7
8
|
end
|
8
9
|
|
10
|
+
self.aliases = {
|
11
|
+
:codec => 'c',
|
12
|
+
:video_codec => 'c:v',
|
13
|
+
:audio_codec => 'c:a',
|
14
|
+
:frame_rate => 'r',
|
15
|
+
:keyframe_interval => 'g',
|
16
|
+
:video_bitrate => 'b:v',
|
17
|
+
:audio_bitrate => 'b:a',
|
18
|
+
:size => 's',
|
19
|
+
:video_filter => 'vf',
|
20
|
+
:format => 'f',
|
21
|
+
:bitstream_format => 'bsf',
|
22
|
+
:pixel_format => 'pix_fmt'
|
23
|
+
}
|
9
24
|
self.bin = '/usr/local/bin/ffmpeg'
|
10
25
|
self.ffprobe_bin = '/usr/local/bin/ffprobe'
|
11
|
-
|
12
|
-
:codec => '-c',
|
13
|
-
:video_codec => '-c:v',
|
14
|
-
:audio_codec => '-c:a',
|
15
|
-
:frame_rate => '-r',
|
16
|
-
:keyint_min => '-keyint_min',
|
17
|
-
:keyframe_interval => '-g',
|
18
|
-
:force_keyframes => '-force_key_frames',
|
19
|
-
:passlogfile => '-passlogfile',
|
20
|
-
:video_bitrate => '-b:v',
|
21
|
-
:audio_bitrate => '-b:a',
|
22
|
-
:size => '-s',
|
23
|
-
:video_filter => '-vf',
|
24
|
-
:threads => '-threads',
|
25
|
-
:format => '-f',
|
26
|
-
:bitstream_format => '-bsf',
|
27
|
-
:pixel_format => '-pix_fmt',
|
28
|
-
:deinterlace => '-deinterlace',
|
29
|
-
:map => '-map',
|
30
|
-
:segment_time => '-segment_time',
|
31
|
-
:reset_timestamps => '-reset_timestamps'
|
32
|
-
}
|
26
|
+
|
33
27
|
self.one_pass_command = '%{bin} -i %{input} -y %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
34
28
|
self.first_pass_command = '%{bin} -i %{input} -y -pass 1 -an %{options} /dev/null 1>>%{log} 2>&1 || exit 1'
|
35
29
|
self.second_pass_command = '%{bin} -i %{input} -y -pass 2 %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
36
30
|
self.keyframes_command = '%{ffprobe_bin} -show_frames -select_streams v:0 -print_format csv %{input} | grep frame,video,1 | cut -d\',\' -f5 | tr "\n" "," | sed \'s/,$//\''
|
37
|
-
self.split_command = '%{bin} -fflags +genpts -i %{input}
|
38
|
-
self.concat_command = "%{bin} -f concat -i %{input}
|
31
|
+
self.split_command = '%{bin} -fflags +genpts -i %{input} %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
32
|
+
self.concat_command = "%{bin} -f concat -i %{input} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
33
|
+
self.mux_command = "%{bin} %{inputs} %{maps} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
34
|
+
|
35
|
+
def self.split(input, output)
|
36
|
+
output.options = { :format => 'segment', :map => 0, :codec => 'copy', :reset_timestamps => 1 }.merge(output.options)
|
37
|
+
Command.new(split_command, prepare_params(input, output)).execute
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.concat(inputs, output, method = nil)
|
41
|
+
method = %w(ts mpg mpeg).include?(File.extname(inputs.first.to_s).delete('.')) ? :protocol : :muxer unless method
|
42
|
+
output.options = { :codec => 'copy' }.merge(output.options)
|
43
|
+
send("concat_#{method}", inputs, output)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.mux(inputs, output)
|
47
|
+
output.options = { :codec => 'copy' }.merge(output.options)
|
48
|
+
Command.new(mux_command, prepare_params(nil, output).merge({
|
49
|
+
:inputs => inputs.map { |i| "-i #{i}" }.join(' '),
|
50
|
+
:maps => inputs.each_with_index.map { |_,i| "-map #{i}:0" }.join(' ')
|
51
|
+
})).execute
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_accessor :input, :outputs
|
39
55
|
|
40
|
-
|
56
|
+
def initialize input, outputs
|
57
|
+
self.input = input
|
58
|
+
self.outputs = input.select_outputs(outputs)
|
41
59
|
|
42
|
-
|
43
|
-
|
44
|
-
|
60
|
+
self.outputs.each do |output|
|
61
|
+
# autorotate
|
62
|
+
if output.type != 'playlist' && [nil, true].include?(output.rotate) && input.metadata[:rotate]
|
63
|
+
output.rotate = 360 - input.metadata[:rotate]
|
64
|
+
end
|
65
|
+
# autodeinterlace
|
66
|
+
output.options[:deinterlace] = input.metadata[:interlaced] if output.options[:deinterlace].nil?
|
67
|
+
# video filter
|
68
|
+
video_filter = [output.options[:video_filter]].compact
|
69
|
+
video_filter << "scale=#{output.width}:trunc\\(ow/a/2\\)*2" if output.width && !output.height
|
70
|
+
video_filter << "scale=trunc\\(oh*a/2\\)*2:#{output.height}" if output.height && !output.width
|
71
|
+
video_filter << { 90 => 'transpose=2', 180 => 'transpose=2,transpose=2', 270 => 'transpose=1' }[output.rotate] if output.rotate
|
72
|
+
output.options[:video_filter] = video_filter.join(',') if video_filter.any?
|
73
|
+
|
74
|
+
output.options[:format] ||= File.extname(output.filename).delete('.')
|
75
|
+
output.options = {
|
76
|
+
:keyint_min => 25,
|
77
|
+
:keyframe_interval => 100,
|
78
|
+
:threads => 1,
|
79
|
+
:video_codec => 'libx264',
|
80
|
+
:audio_codec => 'libfaac',
|
81
|
+
:pixel_format => 'yuv420p'
|
82
|
+
}.merge(output.options) unless output.type == 'playlist'
|
83
|
+
end
|
45
84
|
end
|
46
85
|
|
47
86
|
def run
|
48
87
|
success = true
|
49
88
|
threads = []
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
res = q1.video_bitrate.to_i <=> q2.video_bitrate.to_i
|
89
|
+
|
90
|
+
input.output_groups(outputs).each_with_index do |group, group_index|
|
91
|
+
qualities = group.select { |output| output.type != 'playlist' }
|
92
|
+
|
93
|
+
# common first pass
|
94
|
+
if !one_pass?(qualities) && common_first_pass?(qualities)
|
95
|
+
qualities.each { |output| output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}.log") }
|
96
|
+
best_quality = qualities.sort do |q1, q2|
|
97
|
+
res = q1.options[:video_bitrate].to_i <=> q2.options[:video_bitrate].to_i
|
60
98
|
res = q1.height.to_i <=> q2.height.to_i if res == 0
|
61
99
|
res = q1.width.to_i <=> q2.width.to_i if res == 0
|
100
|
+
# TODO compare by size
|
62
101
|
res
|
63
102
|
end.last
|
64
|
-
success &&= Command.new(self.class.first_pass_command, prepare_params(input, best_quality)).execute
|
65
|
-
end
|
66
|
-
end
|
67
|
-
qualities.each do |output|
|
68
|
-
command = if one_pass
|
69
|
-
self.class.one_pass_command
|
70
|
-
elsif !common_first_pass
|
71
|
-
Command.chain(self.class.first_pass_command, self.class.second_pass_command)
|
72
|
-
else
|
73
|
-
self.class.second_pass_command
|
103
|
+
success &&= Command.new(self.class.first_pass_command, self.class.prepare_params(input, best_quality)).execute
|
74
104
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
105
|
+
|
106
|
+
qualities.each_with_index do |output, output_index|
|
107
|
+
command = if one_pass?(qualities)
|
108
|
+
self.class.one_pass_command
|
109
|
+
elsif common_first_pass?(qualities)
|
110
|
+
self.class.second_pass_command
|
111
|
+
else
|
112
|
+
output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
|
113
|
+
output.options[:force_key_frames] = (input.metadata[:duration_in_ms] / 1000 / Output.keyframe_interval_in_seconds).times.to_a.map { |t| t * Output.keyframe_interval_in_seconds }.join(',')
|
114
|
+
Command.chain(self.class.first_pass_command, self.class.second_pass_command)
|
115
|
+
end
|
116
|
+
|
117
|
+
# run ffmpeg
|
118
|
+
command = Command.new(command, self.class.prepare_params(input, output))
|
119
|
+
if VideoConverter.paral
|
120
|
+
threads << Thread.new { success &&= command.execute }
|
121
|
+
else
|
122
|
+
success &&= command.execute
|
123
|
+
end
|
80
124
|
end
|
81
125
|
end
|
82
126
|
threads.each { |t| t.join } if VideoConverter.paral
|
83
127
|
success
|
84
128
|
end
|
85
129
|
|
86
|
-
|
87
|
-
|
130
|
+
private
|
131
|
+
|
132
|
+
def self.concat_muxer(inputs, output)
|
133
|
+
list = File.join(output.work_dir, 'list.txt')
|
134
|
+
# NOTE ffmpeg concat list requires unescaped files
|
135
|
+
File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.unescape)}'" }.join("\n"))
|
136
|
+
success = Command.new(concat_command, prepare_params(list, output)).execute
|
137
|
+
FileUtils.rm list if success
|
138
|
+
success
|
88
139
|
end
|
89
140
|
|
90
|
-
def
|
91
|
-
Command.new(
|
141
|
+
def self.concat_protocol(inputs, output)
|
142
|
+
Command.new(one_pass_command, prepare_params('"concat:' + inputs.join('|') + '"', output)).execute
|
92
143
|
end
|
93
144
|
|
94
|
-
|
145
|
+
def common_first_pass?(qualities)
|
146
|
+
# if group qualities have different sizes use force_key_frames and separate first passes
|
147
|
+
qualities.uniq { |o| o.height }.count == 1 && qualities.uniq { |o| o.width }.count == 1 && qualities.uniq { |o| o.options[:size] }.count == 1
|
148
|
+
end
|
95
149
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
output.video_filter << "scale=trunc\\(oh*a/2\\)*2:#{output.height}" if output.height && !output.width
|
100
|
-
output.video_filter << { 90 => 'transpose=2', 180 => 'transpose=2,transpose=2', 270 => 'transpose=1' }[output.rotate] if output.rotate
|
101
|
-
output.video_filter = output.video_filter.join(',')
|
150
|
+
def one_pass?(qualities)
|
151
|
+
qualities.inject(true) { |r, q| r && q.one_pass }
|
152
|
+
end
|
102
153
|
|
154
|
+
def self.prepare_params input, output
|
155
|
+
|
103
156
|
{
|
104
|
-
:bin =>
|
157
|
+
:bin => bin,
|
105
158
|
:input => input.to_s,
|
106
|
-
:
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
:segment_time => output.segment_time,
|
111
|
-
:options => self.class.options.map do |output_option, ffmpeg_option|
|
112
|
-
if output.send(output_option).present?
|
113
|
-
if output.send(output_option) == true
|
114
|
-
ffmpeg_option
|
115
|
-
else
|
116
|
-
ffmpeg_option + ' ' + output.send(output_option).to_s
|
117
|
-
end
|
159
|
+
:options => output.options.map do |option, value|
|
160
|
+
if value && !output.respond_to?(option)
|
161
|
+
option = '-' + (aliases[option] || option).to_s
|
162
|
+
value == true ? option : "#{option} #{value}"
|
118
163
|
end
|
119
|
-
end.join(' ')
|
164
|
+
end.compact.join(' '),
|
165
|
+
:output => output.ffmpeg_output,
|
166
|
+
:log => output.log
|
120
167
|
}
|
121
168
|
end
|
122
169
|
end
|
@@ -3,57 +3,18 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Input
|
5
5
|
class << self
|
6
|
-
attr_accessor :metadata_command, :
|
6
|
+
attr_accessor :metadata_command, :show_streams_command, :show_frames_command
|
7
7
|
end
|
8
8
|
|
9
9
|
self.metadata_command = "%{ffprobe_bin} %{input} 2>&1"
|
10
|
-
self.
|
10
|
+
self.show_streams_command = "%{ffprobe_bin} -show_streams -select_streams %{stream} %{input} 2>/dev/null"
|
11
|
+
self.show_frames_command = "%{ffprobe_bin} -show_frames -select_streams %{stream} %{input} 2>/dev/null | head -n 24"
|
11
12
|
|
12
13
|
attr_accessor :input, :output_groups, :metadata
|
13
14
|
|
14
15
|
def initialize input, outputs = []
|
15
|
-
raise ArgumentError.new('
|
16
|
-
self.input = input
|
16
|
+
self.input = input or raise ArgumentError.new('Input requred')
|
17
17
|
raise ArgumentError.new("#{input} does not exist") unless exists?
|
18
|
-
self.metadata = get_metadata
|
19
|
-
|
20
|
-
# for many inputs case take outputs for this input
|
21
|
-
outputs = outputs.select { |output| !output.path || output.path == input.to_s }
|
22
|
-
self.output_groups = []
|
23
|
-
|
24
|
-
# qualities with the same playlist is a one group
|
25
|
-
outputs.select { |output| output.type == 'playlist' }.each_with_index do |playlist, group_index|
|
26
|
-
paths = playlist.streams.map { |stream| stream[:path] }
|
27
|
-
output_group = outputs.select { |output| paths.include?(output.filename) }
|
28
|
-
if output_group.any?
|
29
|
-
# if group qualities have different sizes use force_keyframes and separate first passes
|
30
|
-
common_first_pass = output_group.map { |output| output.height }.uniq.count == 1
|
31
|
-
output_group.each_with_index do |output, output_index|
|
32
|
-
unless output.one_pass
|
33
|
-
if common_first_pass
|
34
|
-
output.passlogfile = File.join(output.work_dir, "group#{group_index}.log")
|
35
|
-
else
|
36
|
-
output.passlogfile = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
|
37
|
-
output.force_keyframes = (metadata[:duration_in_ms] / 1000 / Output.keyframe_interval_in_seconds).times.to_a.map { |t| t * Output.keyframe_interval_in_seconds }.join(',')
|
38
|
-
output.frame_rate = output.keyint_min = output.keyframe_interval = nil
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
self.output_groups << output_group.unshift(playlist)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
# qualities without playlist are separate groups
|
46
|
-
(outputs - output_groups.flatten).each { |output| self.output_groups << [output] }
|
47
|
-
|
48
|
-
# set output parameters depending of input
|
49
|
-
outputs.each do |output|
|
50
|
-
# autorotate
|
51
|
-
if output.type != 'playlist' && [nil, true].include?(output.rotate) && metadata[:rotate]
|
52
|
-
output.rotate = 360 - metadata[:rotate]
|
53
|
-
end
|
54
|
-
# autodeinterlace
|
55
|
-
output.deinterlace = metadata[:interlaced] if output.deinterlace.nil?
|
56
|
-
end
|
57
18
|
end
|
58
19
|
|
59
20
|
def to_s
|
@@ -64,6 +25,70 @@ module VideoConverter
|
|
64
25
|
input.gsub(/\\+([^n])/, '\1')
|
65
26
|
end
|
66
27
|
|
28
|
+
def metadata
|
29
|
+
unless @metadata
|
30
|
+
@metadata = {}
|
31
|
+
# common metadata
|
32
|
+
s = Command.new(self.class.metadata_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
|
33
|
+
if m = s.match(/Stream\s#(\d:\d).*?Audio:\s*(\w+).*?(\d+)\s*Hz.*?(\d+)\s*kb\/s.*?$/)
|
34
|
+
@metadata[:audio_stream] = m[1]
|
35
|
+
@metadata[:audio_codec] = m[2]
|
36
|
+
@metadata[:audio_sample_rate] = m[3].to_i
|
37
|
+
@metadata[:audio_bitrate_in_kbps] = m[4].to_i
|
38
|
+
end
|
39
|
+
@metadata[:channels] = s.scan(/Stream #\d+:\d+/).count
|
40
|
+
if m = s.match(/Duration:\s+(\d+):(\d+):([\d.]+).*?bitrate:\s*(\d+)\s*kb\/s/)
|
41
|
+
@metadata[:duration_in_ms] = ((m[1].to_i * 3600 + m[2].to_i * 60 + m[3].to_f) * 1000).to_i
|
42
|
+
@metadata[:total_bitrate_in_kbps] = m[4].to_i
|
43
|
+
end
|
44
|
+
if m = s.match(/Stream\s#(\d:\d).*?Video:\s(\S+).*?,\s*((\d+)x(\d+))(?:.*?(\d+)\s*kb\/s.*?([\d.]+)\s*fps)?/)
|
45
|
+
@metadata[:video_stream] = m[1]
|
46
|
+
@metadata[:video_codec] = m[2]
|
47
|
+
@metadata[:width] = m[4].to_i
|
48
|
+
@metadata[:height] = m[5].to_i
|
49
|
+
@metadata[:video_bitrate_in_kbps] = m[6].to_i
|
50
|
+
@metadata[:frame_rate] = m[7].to_f
|
51
|
+
end
|
52
|
+
if m = s.match(/rotate\s*\:\s*(\d+)/)
|
53
|
+
@metadata[:rotate] = m[1].to_i
|
54
|
+
end
|
55
|
+
if is_http?
|
56
|
+
url = URI.parse(input)
|
57
|
+
Net::HTTP.start(url.host) do |http|
|
58
|
+
response = http.request_head url.path
|
59
|
+
@metadata[:file_size_in_bytes] = response['content-length'].to_i
|
60
|
+
end
|
61
|
+
elsif is_local?
|
62
|
+
@metadata[:file_size_in_bytes] = File.size(input.gsub('\\', ''))
|
63
|
+
end
|
64
|
+
@metadata[:format] = File.extname(input).delete('.')
|
65
|
+
|
66
|
+
# stream metadata
|
67
|
+
s = Command.new(self.class.show_streams_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :stream => 'v', :input => input).capture
|
68
|
+
@metadata[:video_start_time] = s.match(/start_time=([\d.]+)/).to_a[1].to_f
|
69
|
+
|
70
|
+
# frame metadata
|
71
|
+
s = Command.new(self.class.show_frames_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :stream => 'v', :input => input).capture
|
72
|
+
@metadata[:interlaced] = true if s.include?('interlaced_frame=1')
|
73
|
+
end
|
74
|
+
@metadata
|
75
|
+
end
|
76
|
+
|
77
|
+
def select_outputs(outputs)
|
78
|
+
outputs.select { |output| !output.path || output.path == input }
|
79
|
+
end
|
80
|
+
|
81
|
+
def output_groups(outputs)
|
82
|
+
groups = []
|
83
|
+
outputs.select { |output| output.type == 'playlist' }.each do |playlist|
|
84
|
+
paths = playlist.streams.map { |stream| stream[:path] }
|
85
|
+
groups << outputs.select { |output| paths.include?(output.filename) }.unshift(playlist)
|
86
|
+
end
|
87
|
+
# qualities without playlist are separate groups
|
88
|
+
(outputs - groups.flatten).each { |output| groups << [output] }
|
89
|
+
groups
|
90
|
+
end
|
91
|
+
|
67
92
|
private
|
68
93
|
|
69
94
|
def exists?
|
@@ -87,46 +112,5 @@ module VideoConverter
|
|
87
112
|
File.file?(input.gsub('\\', ''))
|
88
113
|
end
|
89
114
|
|
90
|
-
def get_metadata
|
91
|
-
metadata = {}
|
92
|
-
# common metadata
|
93
|
-
s = Command.new(self.class.metadata_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
|
94
|
-
if m = s.match(/Stream.*?Audio:\s*(\w+).*?(\d+)\s*Hz.*?(\d+)\s*kb\/s.*?$/)
|
95
|
-
metadata[:audio_codec] = m[1]
|
96
|
-
metadata[:audio_sample_rate] = m[2].to_i
|
97
|
-
metadata[:audio_bitrate_in_kbps] = m[3].to_i
|
98
|
-
end
|
99
|
-
metadata[:channels] = s.scan(/Stream #\d+:\d+/).count
|
100
|
-
if m = s.match(/Duration:\s+(\d+):(\d+):([\d.]+).*?bitrate:\s*(\d+)\s*kb\/s/)
|
101
|
-
metadata[:duration_in_ms] = ((m[1].to_i * 3600 + m[2].to_i * 60 + m[3].to_f) * 1000).to_i
|
102
|
-
metadata[:total_bitrate_in_kbps] = m[4].to_i
|
103
|
-
end
|
104
|
-
if m = s.match(/Stream.*?Video:\s(\S+).*?,\s*((\d+)x(\d+)).*?(\d+)\s*kb\/s.*?([\d.]+)\s*fps/)
|
105
|
-
metadata[:video_codec] = m[1]
|
106
|
-
metadata[:width] = m[3].to_i
|
107
|
-
metadata[:height] = m[4].to_i
|
108
|
-
metadata[:video_bitrate_in_kbps] = m[5].to_i
|
109
|
-
metadata[:frame_rate] = m[6].to_f
|
110
|
-
end
|
111
|
-
if m = s.match(/rotate\s*\:\s*(\d+)/)
|
112
|
-
metadata[:rotate] = m[1].to_i
|
113
|
-
end
|
114
|
-
if is_http?
|
115
|
-
url = URI.parse(input)
|
116
|
-
Net::HTTP.start(url.host) do |http|
|
117
|
-
response = http.request_head url.path
|
118
|
-
metadata[:file_size_in_bytes] = response['content-length'].to_i
|
119
|
-
end
|
120
|
-
elsif is_local?
|
121
|
-
metadata[:file_size_in_bytes] = File.size(input.gsub('\\', ''))
|
122
|
-
end
|
123
|
-
metadata[:format] = File.extname(input).delete('.')
|
124
|
-
|
125
|
-
# frame metadata
|
126
|
-
s = Command.new(self.class.show_frame_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
|
127
|
-
metadata[:interlaced] = true if s.include?('interlaced_frame=1')
|
128
|
-
|
129
|
-
metadata
|
130
|
-
end
|
131
115
|
end
|
132
116
|
end
|
@@ -13,63 +13,56 @@ module VideoConverter
|
|
13
13
|
|
14
14
|
self.command = '%{ffmpeg_bin} -i %{ffmpeg_output} -c:v copy -c:a copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{keyframe_interval_in_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
def initialize input, group
|
19
|
-
self.input = input
|
20
|
-
self.group = group
|
21
|
-
end
|
22
|
-
|
23
|
-
def run
|
16
|
+
def self.run(input, outputs)
|
24
17
|
success = true
|
25
18
|
threads = []
|
26
19
|
p = Proc.new do |output|
|
27
20
|
make_chunks(output) && gen_quality_playlist(output)
|
28
21
|
end
|
29
|
-
|
22
|
+
outputs.select { |output| output.type != 'playlist' }.each do |output|
|
30
23
|
if VideoConverter.paral
|
31
24
|
threads << Thread.new { success &&= p.call(output) }
|
32
25
|
else
|
33
26
|
success &&= p.call(output)
|
34
27
|
end
|
35
28
|
end
|
36
|
-
success &&= gen_group_playlist(
|
29
|
+
success &&= gen_group_playlist(outputs.detect { |output| output.type == 'playlist' })
|
37
30
|
threads.each { |t| t.join } if VideoConverter.paral
|
38
31
|
success
|
39
32
|
end
|
40
33
|
|
41
34
|
private
|
42
35
|
|
43
|
-
def make_chunks output
|
44
|
-
Command.new(
|
36
|
+
def self.make_chunks output
|
37
|
+
Command.new(command, prepare_params(output)).execute
|
45
38
|
end
|
46
39
|
|
47
|
-
def gen_quality_playlist output
|
40
|
+
def self.gen_quality_playlist output
|
48
41
|
res = ''
|
49
42
|
durations = []
|
50
43
|
# order desc
|
51
|
-
chunks = Dir::glob(File.join(output.chunks_dir, "#{
|
44
|
+
chunks = Dir::glob(File.join(output.chunks_dir, "#{chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
|
52
45
|
File.basename(c2).match(/\d+/).to_s.to_i <=> File.basename(c1).match(/\d+/).to_s.to_i
|
53
46
|
end
|
54
47
|
# chunk duration = (pts of first frame of the next chunk - pts of first frame of current chunk) / time_base
|
55
48
|
# for the last chunks the last two pts are used
|
56
|
-
prl_pts, l_pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{
|
49
|
+
prl_pts, l_pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunks.first} | tail -n2 2>&1`.split("\n").map { |l| l.split(',')[3].to_i }
|
57
50
|
# NOTE for case when chunk has one frame
|
58
51
|
l_pts ||= prl_pts
|
59
52
|
next_chunk_pts = 2 * l_pts - prl_pts
|
60
|
-
time_base = `#{Ffmpeg.ffprobe_bin} -show_streams -select_streams #{
|
53
|
+
time_base = `#{Ffmpeg.ffprobe_bin} -show_streams -select_streams #{select_streams} -loglevel fatal #{chunks.first} 2>&1`.match(/\ntime_base=1\/(\d+)/)[1].to_f
|
61
54
|
chunks.each do |chunk|
|
62
|
-
pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{
|
55
|
+
pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunk} | head -n1 2>&1`.split(',')[3].to_i
|
63
56
|
durations << (duration = (next_chunk_pts - pts) / time_base)
|
64
57
|
next_chunk_pts = pts
|
65
58
|
res = File.join(File.basename(output.chunks_dir), File.basename(chunk)) + "\n" + res
|
66
59
|
res = "#EXTINF:%0.2f,\n" % duration + res
|
67
60
|
end
|
68
|
-
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
|
61
|
+
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max.to_f.ceil}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
|
69
62
|
!!File.open(File.join(output.work_dir, output.filename), 'w') { |f| f.write res }
|
70
63
|
end
|
71
64
|
|
72
|
-
def gen_group_playlist playlist
|
65
|
+
def self.gen_group_playlist playlist
|
73
66
|
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n"
|
74
67
|
playlist.streams.sort { |s1, s2| s1[:bandwidth].to_i <=> s2[:bandwidth].to_i }.each do |stream|
|
75
68
|
res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{stream[:bandwidth].to_i * 1000}\n"
|
@@ -80,17 +73,17 @@ module VideoConverter
|
|
80
73
|
true
|
81
74
|
end
|
82
75
|
|
83
|
-
def prepare_params output
|
76
|
+
def self.prepare_params output
|
84
77
|
{
|
85
78
|
:ffmpeg_bin => Ffmpeg.bin,
|
86
79
|
:ffmpeg_output => output.ffmpeg_output,
|
87
|
-
:bin =>
|
80
|
+
:bin => bin,
|
88
81
|
:keyframe_interval_in_seconds => Output.keyframe_interval_in_seconds,
|
89
82
|
:chunks_dir => output.chunks_dir,
|
90
|
-
:chunk_prefix =>
|
91
|
-
:encoding_profile =>
|
83
|
+
:chunk_prefix => chunk_prefix,
|
84
|
+
:encoding_profile => encoding_profile,
|
92
85
|
:log => output.log
|
93
86
|
}
|
94
87
|
end
|
95
88
|
end
|
96
|
-
end
|
89
|
+
end
|
@@ -1,24 +1,17 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module VideoConverter
|
4
|
-
class
|
4
|
+
class Mp4frag
|
5
5
|
class << self
|
6
6
|
attr_accessor :bin, :command
|
7
7
|
end
|
8
8
|
self.bin = '/usr/local/bin/mp4frag'
|
9
9
|
self.command = '%{bin} %{inputs} --manifest %{manifest} --index 1>>%{log} 2>&1 || exit 1'
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
def initialize input, group
|
14
|
-
self.input = input
|
15
|
-
self.group = group
|
16
|
-
end
|
17
|
-
|
18
|
-
def run
|
11
|
+
def self.run(input, outputs)
|
19
12
|
success = true
|
20
13
|
threads = []
|
21
|
-
command = Command.new(self.
|
14
|
+
command = Command.new(self.command, prepare_params(outputs))
|
22
15
|
if VideoConverter.paral
|
23
16
|
threads << Thread.new { success &&= command.execute }
|
24
17
|
else
|
@@ -30,12 +23,12 @@ module VideoConverter
|
|
30
23
|
|
31
24
|
private
|
32
25
|
|
33
|
-
def prepare_params(
|
26
|
+
def self.prepare_params(outputs)
|
34
27
|
{
|
35
|
-
:bin =>
|
36
|
-
:inputs =>
|
37
|
-
:manifest =>
|
38
|
-
:log =>
|
28
|
+
:bin => bin,
|
29
|
+
:inputs => outputs.select { |output| output.type != 'playlist' }.map { |input| "--src #{input.ffmpeg_output}" }.join(' '),
|
30
|
+
:manifest => outputs.detect { |output| output.type == 'playlist' }.ffmpeg_output,
|
31
|
+
:log => outputs.first.log
|
39
32
|
}
|
40
33
|
end
|
41
34
|
end
|
@@ -3,98 +3,34 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Output
|
5
5
|
class << self
|
6
|
-
attr_accessor :work_dir, :
|
6
|
+
attr_accessor :work_dir, :log, :keyframe_interval_in_seconds
|
7
7
|
end
|
8
|
-
|
9
8
|
self.work_dir = '/tmp'
|
10
|
-
self.
|
11
|
-
self.keyframe_interval = 100
|
9
|
+
self.log = 'converter.log'
|
12
10
|
self.keyframe_interval_in_seconds = 4
|
13
|
-
self.threads = 1
|
14
|
-
self.video_codec = 'libx264'
|
15
|
-
self.audio_codec = 'libfaac'
|
16
|
-
self.pixel_format = 'yuv420p'
|
17
11
|
|
18
|
-
attr_accessor :
|
19
|
-
attr_accessor :type, :filename
|
20
|
-
attr_accessor :format, :ffmpeg_output, :codec, :video_codec, :audio_codec, :bitstream_format, :pixel_format, :map
|
21
|
-
attr_accessor :one_pass, :video_bitrate, :audio_bitrate
|
22
|
-
attr_accessor :streams, :path, :chunks_dir, :segment_time, :reset_timestamps
|
23
|
-
attr_accessor :frame_rate, :keyint_min, :keyframe_interval, :force_keyframes
|
24
|
-
attr_accessor :size, :width, :height, :video_filter
|
25
|
-
attr_accessor :thumbnails
|
26
|
-
attr_accessor :rotate, :deinterlace
|
27
|
-
attr_accessor :faststart
|
12
|
+
attr_accessor :work_dir, :filename, :log, :type, :chunks_dir, :ffmpeg_output, :path, :streams, :width, :height, :one_pass, :rotate, :faststart, :thumbnails, :uid, :options
|
28
13
|
|
29
14
|
def initialize params = {}
|
30
|
-
|
31
|
-
self.uid = params[:uid].to_s
|
32
|
-
self.work_dir = File.join(self.class.work_dir, uid)
|
15
|
+
self.work_dir = File.join(self.class.work_dir, params[:uid])
|
33
16
|
FileUtils.mkdir_p(work_dir)
|
34
|
-
self.
|
35
|
-
self.
|
36
|
-
|
37
|
-
|
38
|
-
# General output options
|
39
|
-
self.type = params[:type]
|
40
|
-
raise ArgumentError.new('Incorrect type') if type && !%w(default segmented playlist).include?(type)
|
41
|
-
self.filename = params[:filename] or raise ArgumentError.new('filename required')
|
42
|
-
|
43
|
-
# Formats and codecs
|
44
|
-
if type == 'segmented'
|
45
|
-
self.format = 'mpegts'
|
46
|
-
self.ffmpeg_output = File.join(work_dir, File.basename(filename, '.*') + '.ts')
|
47
|
-
else
|
48
|
-
self.format = params[:format] || File.extname(filename).sub('.', '')
|
49
|
-
self.ffmpeg_output = File.join(work_dir, filename)
|
50
|
-
raise ArgumentError.new('Invalid playlist extension') if type == 'playlist' && !['f4m', 'm3u8'].include?(format)
|
51
|
-
end
|
52
|
-
self.codec = params[:codec]
|
53
|
-
self.video_codec = params[:video_codec] || self.class.video_codec unless codec
|
54
|
-
self.audio_codec = params[:audio_codec] || self.class.audio_codec unless codec
|
55
|
-
self.bitstream_format = params[:bitstream_format]
|
56
|
-
self.pixel_format = params[:pixel_format] || self.class.pixel_format
|
57
|
-
self.map = params[:map]
|
58
|
-
|
59
|
-
# Rate controle
|
60
|
-
self.one_pass = !!params[:one_pass]
|
61
|
-
self.video_bitrate = "#{params[:video_bitrate]}k" if params[:video_bitrate]
|
62
|
-
self.audio_bitrate = "#{params[:audio_bitrate]}k" if params[:audio_bitrate]
|
63
|
-
|
64
|
-
# Segmented streaming
|
65
|
-
self.streams = params[:streams]
|
66
|
-
self.path = params[:path]
|
17
|
+
self.filename = params[:filename] or raise ArgumentError.new('Filename required')
|
18
|
+
self.log = File.join(work_dir, self.class.log)
|
19
|
+
self.type = params[:type] || 'default'
|
67
20
|
if type == 'segmented'
|
68
21
|
self.chunks_dir = File.join(work_dir, File.basename(filename, '.*'))
|
69
22
|
FileUtils.mkdir_p(chunks_dir)
|
23
|
+
self.ffmpeg_output = chunks_dir + '.ts'
|
24
|
+
params[:format] = 'mpegts'
|
25
|
+
else
|
26
|
+
self.ffmpeg_output = File.join(work_dir, filename)
|
70
27
|
end
|
71
|
-
|
72
|
-
self.
|
73
|
-
|
74
|
-
# Frame rate
|
75
|
-
self.frame_rate = params[:frame_rate]
|
76
|
-
self.keyint_min = params[:keyint_min] || self.class.keyint_min
|
77
|
-
self.keyframe_interval = params[:keyframe_interval] || self.class.keyframe_interval
|
78
|
-
|
79
|
-
# Resolution
|
80
|
-
self.size = params[:size]
|
81
|
-
self.width = params[:size] ? params[:size].split('x').first : params[:width]
|
82
|
-
self.height = params[:size] ? params[:size].split('x').last : params[:height]
|
83
|
-
self.size = "#{width}x#{height}" if !size && width && height
|
84
|
-
|
85
|
-
#Thumbnails
|
86
|
-
self.thumbnails = params[:thumbnails]
|
87
|
-
|
88
|
-
# Video processing
|
89
|
-
self.rotate = params[:rotate]
|
90
|
-
unless [nil, true, false].include? rotate
|
91
|
-
self.rotate = rotate.to_i
|
92
|
-
raise ArgumentError.new('Invalid rotate') unless [0, 90, 180, 270].include? rotate
|
93
|
-
end
|
94
|
-
self.deinterlace = params[:deinterlace]
|
28
|
+
raise ArgumentError.new('Invalid type') unless %w(default segmented playlist).include?(type)
|
29
|
+
[:path, :streams, :width, :height, :one_pass, :rotate, :faststart, :thumbnails].each { |attr| self.send("#{attr}=", params[attr]) }
|
30
|
+
[:video_bitrate, :audio_bitrate].each { |bitrate| params[bitrate] = "#{params[bitrate]}k" if params[bitrate].is_a?(Numeric) }
|
95
31
|
|
96
|
-
#
|
97
|
-
self.
|
32
|
+
# options will be substituted to convertation commands
|
33
|
+
self.options = params
|
98
34
|
end
|
99
35
|
end
|
100
36
|
end
|
@@ -18,11 +18,11 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
18
18
|
|
19
19
|
should 'create qualities and thumbnails' do
|
20
20
|
3.times.each do |n|
|
21
|
-
q = File.join(
|
21
|
+
q = File.join(@c.outputs.first.work_dir, "q#{n + 1}.mp4")
|
22
22
|
assert File.exists?(q)
|
23
23
|
assert File.size(q) > 0
|
24
24
|
end
|
25
|
-
assert_equal %w(. .. scr004.jpg scr004_norm.jpg scr005.jpg scr005_norm.jpg).sort, Dir.entries(File.join(
|
25
|
+
assert_equal %w(. .. scr004.jpg scr004_norm.jpg scr005.jpg scr005_norm.jpg).sort, Dir.entries(File.join(@c.outputs.first.work_dir, 'thumbnails')).sort
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -52,10 +52,10 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
52
52
|
should 'generate hls' do
|
53
53
|
%w(sd1 sd2 hd1 hd2).each do |quality|
|
54
54
|
# should create chunks
|
55
|
-
assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(
|
55
|
+
assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
56
56
|
# TODO verify that chunks have different quality (weight)
|
57
57
|
# should create playlists
|
58
|
-
assert File.exists?(playlist = File.join(
|
58
|
+
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
59
59
|
# TODO verify that playlist is valid (contain all chunks and modifiers)
|
60
60
|
end
|
61
61
|
end
|
@@ -83,12 +83,12 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
83
83
|
|
84
84
|
should 'generate hds with sync keyframes' do
|
85
85
|
%w(sd1.mp4 sd2.mp4 playlist.f4m hd1.mp4 hd2.mp4 hd_playlist.f4m).each do |filename|
|
86
|
-
assert File.exists?(File.join(
|
86
|
+
assert File.exists?(File.join(@c.outputs.first.work_dir, "#{filename}"))
|
87
87
|
end
|
88
88
|
|
89
89
|
assert_equal(
|
90
|
-
(k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(
|
91
|
-
(k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(
|
90
|
+
(k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(@c.outputs.first.work_dir, 'sd1.mp4')).capture),
|
91
|
+
(k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(@c.outputs.first.work_dir, 'sd2.mp4')).capture)
|
92
92
|
)
|
93
93
|
end
|
94
94
|
end
|
@@ -114,10 +114,10 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
114
114
|
should 'generate hls' do
|
115
115
|
%w(q1 q2).each do |quality|
|
116
116
|
# should create chunks
|
117
|
-
assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(
|
117
|
+
assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
118
118
|
# TODO verify that chunks have the same quality (weight)
|
119
119
|
# should create playlists
|
120
|
-
assert File.exists?(playlist = File.join(
|
120
|
+
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
121
121
|
# TODO verify that playlist is valid (contain all chunks and modifiers)
|
122
122
|
end
|
123
123
|
end
|
@@ -128,7 +128,7 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
128
128
|
setup do
|
129
129
|
(@c = VideoConverter.new(
|
130
130
|
:input => "test/fixtures/test (1).mp4",
|
131
|
-
:output => { :segment_time => 2, :filename => '%01d.nut' }
|
131
|
+
:output => { :segment_time => 2, :codec => 'copy', :filename => '%01d.nut' }
|
132
132
|
)).split
|
133
133
|
end
|
134
134
|
should 'segment file' do
|
@@ -153,4 +153,22 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
153
153
|
)
|
154
154
|
end
|
155
155
|
end
|
156
|
+
|
157
|
+
# context 'muxing' do
|
158
|
+
# setup do
|
159
|
+
# input = "test/fixtures/test (1).mp4"
|
160
|
+
# (c1 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:0', :filename => 'video.mp4' })).convert
|
161
|
+
# (c2 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:1', :filename => 'audio.wav' })).convert
|
162
|
+
# (@c = VideoConverter.new(:uid => 'test', :input => [c1.outputs.first.ffmpeg_output, c2.outputs.first.ffmpeg_output], :output => { :filename => 'mux.mp4' })).mux
|
163
|
+
# @metadata = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.metadata
|
164
|
+
# end
|
165
|
+
# should 'mux streams' do
|
166
|
+
# assert File.exists?(@c.outputs.first.ffmpeg_output)
|
167
|
+
# assert_equal '0:0', @metadata[:video_stream]
|
168
|
+
# assert_equal '0:1', @metadata[:audio_stream]
|
169
|
+
# end
|
170
|
+
# teardown do
|
171
|
+
# #FileUtils.rm_r @c.outputs.first.work_dir
|
172
|
+
# end
|
173
|
+
# end
|
156
174
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: video_converter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-06-
|
12
|
+
date: 2014-06-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: video_screenshoter
|
@@ -112,10 +112,10 @@ files:
|
|
112
112
|
- lib/video_converter/faststart.rb
|
113
113
|
- lib/video_converter/ffmpeg.rb
|
114
114
|
- lib/video_converter/hash.rb
|
115
|
-
- lib/video_converter/hds.rb
|
116
115
|
- lib/video_converter/hls.rb
|
117
116
|
- lib/video_converter/input.rb
|
118
117
|
- lib/video_converter/live_segmenter.rb
|
118
|
+
- lib/video_converter/mp4frag.rb
|
119
119
|
- lib/video_converter/object.rb
|
120
120
|
- lib/video_converter/output.rb
|
121
121
|
- lib/video_converter/version.rb
|
@@ -142,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
142
|
version: '0'
|
143
143
|
segments:
|
144
144
|
- 0
|
145
|
-
hash: -
|
145
|
+
hash: -4263200724433938900
|
146
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
147
|
none: false
|
148
148
|
requirements:
|
@@ -151,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
151
|
version: '0'
|
152
152
|
segments:
|
153
153
|
- 0
|
154
|
-
hash: -
|
154
|
+
hash: -4263200724433938900
|
155
155
|
requirements:
|
156
156
|
- ffmpeg, version 1.2 or greated configured with libx264 and libfaac
|
157
157
|
- live_segmenter to convert to hls
|