video_converter 0.6.2 → 0.6.4
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/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
|