video_converter 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/video_converter.rb +1 -2
- data/lib/video_converter/base.rb +13 -20
- data/lib/video_converter/command.rb +12 -2
- data/lib/video_converter/f4fpackager.rb +3 -3
- data/lib/video_converter/ffmpeg.rb +66 -56
- data/lib/video_converter/hash.rb +31 -8
- data/lib/video_converter/input.rb +14 -6
- data/lib/video_converter/live_segmenter.rb +1 -1
- data/lib/video_converter/mp4frag.rb +1 -1
- data/lib/video_converter/openssl.rb +1 -1
- data/lib/video_converter/version.rb +1 -1
- data/test/fixtures/logo.png +0 -0
- data/test/fixtures/test_crop.mp4 +0 -0
- data/test/video_converter_test.rb +36 -2
- metadata +8 -5
- data/lib/video_converter/faststart.rb +0 -35
data/lib/video_converter.rb
CHANGED
@@ -5,7 +5,6 @@ require "video_converter/array"
|
|
5
5
|
require "video_converter/base"
|
6
6
|
require "video_converter/command"
|
7
7
|
require "video_converter/f4fpackager"
|
8
|
-
require "video_converter/faststart"
|
9
8
|
require "video_converter/ffmpeg"
|
10
9
|
require "video_converter/hash"
|
11
10
|
require "video_converter/input"
|
@@ -27,6 +26,6 @@ module VideoConverter
|
|
27
26
|
self.paral = true
|
28
27
|
|
29
28
|
def self.new params
|
30
|
-
VideoConverter::Base.new params.deep_symbolize_keys
|
29
|
+
VideoConverter::Base.new params.deep_symbolize_keys
|
31
30
|
end
|
32
31
|
end
|
data/lib/video_converter/base.rb
CHANGED
@@ -10,7 +10,7 @@ module VideoConverter
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def run
|
13
|
-
convert &&
|
13
|
+
convert && make_screenshots && segment && encrypt && clear
|
14
14
|
end
|
15
15
|
|
16
16
|
# XXX inject instead of each would be better
|
@@ -20,13 +20,6 @@ module VideoConverter
|
|
20
20
|
success
|
21
21
|
end
|
22
22
|
|
23
|
-
# TODO use for faststart ffmpeg moveflags
|
24
|
-
def faststart
|
25
|
-
success = true
|
26
|
-
outputs.each { |output| success &&= Faststart.new(output).run if output.faststart }
|
27
|
-
success
|
28
|
-
end
|
29
|
-
|
30
23
|
def make_screenshots
|
31
24
|
success = true
|
32
25
|
outputs.each do |output|
|
@@ -45,18 +38,6 @@ module VideoConverter
|
|
45
38
|
success
|
46
39
|
end
|
47
40
|
|
48
|
-
def split
|
49
|
-
Ffmpeg.split(inputs.first, outputs.first)
|
50
|
-
end
|
51
|
-
|
52
|
-
def concat method = nil
|
53
|
-
Ffmpeg.concat(inputs, outputs.first, method)
|
54
|
-
end
|
55
|
-
|
56
|
-
def mux
|
57
|
-
Ffmpeg.mux(inputs, outputs.first)
|
58
|
-
end
|
59
|
-
|
60
41
|
def encrypt(options = {})
|
61
42
|
outputs.each do |output|
|
62
43
|
case output.drm
|
@@ -77,5 +58,17 @@ module VideoConverter
|
|
77
58
|
outputs.select { |output| output.type == 'segmented' }.each { |output| Command.new("rm #{output.ffmpeg_output}").execute }
|
78
59
|
true
|
79
60
|
end
|
61
|
+
|
62
|
+
def split
|
63
|
+
Ffmpeg.split(inputs.first, outputs.first)
|
64
|
+
end
|
65
|
+
|
66
|
+
def concat method = nil
|
67
|
+
Ffmpeg.concat(inputs, outputs.first, method)
|
68
|
+
end
|
69
|
+
|
70
|
+
def mux
|
71
|
+
Ffmpeg.mux(inputs, outputs.first)
|
72
|
+
end
|
80
73
|
end
|
81
74
|
end
|
@@ -14,9 +14,19 @@ module VideoConverter
|
|
14
14
|
|
15
15
|
attr_accessor :command
|
16
16
|
|
17
|
-
def initialize command,
|
17
|
+
def initialize command, params = {}, safe_keys = []
|
18
18
|
self.command = command.dup
|
19
|
-
|
19
|
+
if params.any?
|
20
|
+
params = params.deep_shellescape_values(safe_keys)
|
21
|
+
self.command.gsub!(/%\{(\w+?)\}/) do
|
22
|
+
value = params[$1.to_sym]
|
23
|
+
if value.is_a?(Hash)
|
24
|
+
value.deep_join(' ')
|
25
|
+
else
|
26
|
+
value.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
20
30
|
raise ArgumentError.new("Command is not parsed '#{self.command}'") if self.command.match(/%{[\w\-.]+}/)
|
21
31
|
end
|
22
32
|
|
@@ -41,11 +41,11 @@ module VideoConverter
|
|
41
41
|
{
|
42
42
|
:bin => bin,
|
43
43
|
:input_file => options[:input_file],
|
44
|
-
:options => allowed_options.map do |option|
|
44
|
+
:options => Hash[*allowed_options.map do |option|
|
45
45
|
if value = options[option.gsub('-', '_').to_sym]
|
46
|
-
'--' + option
|
46
|
+
['--' + option, value]
|
47
47
|
end
|
48
|
-
end.compact.
|
48
|
+
end.compact.flatten],
|
49
49
|
:log => options[:log]
|
50
50
|
}
|
51
51
|
end
|
@@ -4,7 +4,7 @@ module VideoConverter
|
|
4
4
|
class Ffmpeg
|
5
5
|
class << self
|
6
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, :volume_detect_command
|
7
|
+
attr_accessor :one_pass_command, :first_pass_command, :second_pass_command, :keyframes_command, :split_command, :concat_command, :mux_command, :volume_detect_command, :crop_detect_command
|
8
8
|
end
|
9
9
|
|
10
10
|
self.aliases = {
|
@@ -24,14 +24,15 @@ module VideoConverter
|
|
24
24
|
self.bin = '/usr/local/bin/ffmpeg'
|
25
25
|
self.ffprobe_bin = '/usr/local/bin/ffprobe'
|
26
26
|
|
27
|
-
self.one_pass_command = '%{bin}
|
28
|
-
self.first_pass_command = '%{bin}
|
29
|
-
self.second_pass_command = '%{bin}
|
30
|
-
self.keyframes_command = '%{ffprobe_bin} -show_frames -select_streams v:0 -print_format csv %{
|
31
|
-
self.split_command = '%{bin} -fflags +genpts
|
32
|
-
self.concat_command = "%{bin} -f concat
|
27
|
+
self.one_pass_command = '%{bin} %{inputs} -y %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
28
|
+
self.first_pass_command = '%{bin} %{inputs} -y -pass 1 -an %{options} /dev/null 1>>%{log} 2>&1 || exit 1'
|
29
|
+
self.second_pass_command = '%{bin} %{inputs} -y -pass 2 %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
30
|
+
self.keyframes_command = '%{ffprobe_bin} -show_frames -select_streams v:0 -print_format csv %{inputs} | grep frame,video,1 | cut -d\',\' -f5 | tr "\n" "," | sed \'s/,$//\''
|
31
|
+
self.split_command = '%{bin} -fflags +genpts %{inputs} %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
32
|
+
self.concat_command = "%{bin} -f concat %{inputs} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
33
33
|
self.mux_command = "%{bin} %{inputs} %{maps} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
34
34
|
self.volume_detect_command = "%{bin} -i %{input} -af volumedetect -c:v copy -f null - 2>&1"
|
35
|
+
self.crop_detect_command = "%{bin} -ss %{ss} -i %{input} -vframes %{vframes} -vf cropdetect=round=2 -c:a copy -f null - 2>&1"
|
35
36
|
|
36
37
|
def self.split(input, output)
|
37
38
|
output.options = { :format => 'segment', :map => 0, :codec => 'copy' }.merge(output.options)
|
@@ -47,8 +48,8 @@ module VideoConverter
|
|
47
48
|
def self.mux(inputs, output)
|
48
49
|
output.options = { :codec => 'copy' }.merge(output.options)
|
49
50
|
Command.new(mux_command, prepare_params(nil, output).merge({
|
50
|
-
:inputs =>
|
51
|
-
:maps => inputs.each_with_index.map { |_,i| "
|
51
|
+
:inputs => { '-i' => inputs },
|
52
|
+
:maps => { '-map' => inputs.each_with_index.map { |_,i| "#{i}:0" }.join(' ') }
|
52
53
|
})).execute
|
53
54
|
end
|
54
55
|
|
@@ -59,43 +60,51 @@ module VideoConverter
|
|
59
60
|
self.outputs = input.select_outputs(outputs)
|
60
61
|
|
61
62
|
self.outputs.each do |output|
|
62
|
-
# autorotate
|
63
|
-
if output.type != 'playlist' && [nil, true].include?(output.rotate) && input.metadata[:rotate]
|
64
|
-
output.rotate = 360 - input.metadata[:rotate]
|
65
|
-
end
|
66
|
-
# autodeinterlace
|
67
|
-
output.options[:deinterlace] = input.metadata[:interlaced] if output.options[:deinterlace].nil?
|
68
63
|
# volume
|
69
64
|
output.options[:audio_filter] = "volume=#{volume(output.volume)}" if output.volume
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
output.width = (output.height * aspect(input, output)).ceil / 2 * 2 if output.height && !output.width
|
75
|
-
output.height = (output.width / aspect(input, output)).ceil / 2 * 2 if output.width && !output.height
|
76
|
-
filter_complex << "scale=#{scale(output.width, :w)}:#{scale(output.height, :h)}"
|
77
|
-
if output.options[:aspect]
|
78
|
-
filter_complex << "setdar=#{output.options.delete(:aspect)}"
|
79
|
-
elsif input.video_stream[:dar_width] && input.video_stream[:dar_height]
|
80
|
-
filter_complex << "setdar=#{input.video_stream[:dar_width]}:#{input.video_stream[:dar_height]}"
|
65
|
+
unless output.options[:vn]
|
66
|
+
# autorotate
|
67
|
+
if output.type != 'playlist' && output.rotate == true
|
68
|
+
output.rotate = input.metadata[:rotate] ? 360 - input.metadata[:rotate] : nil
|
81
69
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
filter_complex
|
87
|
-
|
88
|
-
|
89
|
-
|
70
|
+
# autocrop
|
71
|
+
output.crop = input.crop_detect if output.type != 'playlist' && output.crop == true
|
72
|
+
# autodeinterlace
|
73
|
+
output.options[:deinterlace] = input.metadata[:interlaced] if output.options[:deinterlace].nil?
|
74
|
+
# filter_complex
|
75
|
+
filter_complex = []
|
76
|
+
filter_complex << "crop=#{output.crop.shellescape}" if output.crop
|
77
|
+
if output.width || output.height
|
78
|
+
output.width = (output.height * aspect(input, output)).ceil / 2 * 2 if output.height && !output.width
|
79
|
+
output.height = (output.width / aspect(input, output)).ceil / 2 * 2 if output.width && !output.height
|
80
|
+
filter_complex << "scale=#{scale(output.width, :w)}:#{scale(output.height, :h)}"
|
81
|
+
if output.options[:aspect]
|
82
|
+
filter_complex << "setdar=#{output.options.delete(:aspect).to_s.shellescape}"
|
83
|
+
elsif input.video_stream[:dar_width] && input.video_stream[:dar_height]
|
84
|
+
filter_complex << "setdar=#{aspect(input, output)}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
if output.watermarks && (output.watermarks[:width] || output.watermarks[:height])
|
88
|
+
filter_complex = ["[0:v] #{filter_complex.join(',')} [main]"]
|
89
|
+
filter_complex << "[1:v] scale=#{scale(output.watermarks[:width], :w, output.width)}:#{scale(output.watermarks[:height], :h, output.height)} [overlay]"
|
90
|
+
filter_complex << "[main] [overlay] overlay=#{overlay(output.watermarks[:x], :w)}:#{overlay(output.watermarks[:y], :h)}"
|
91
|
+
if output.rotate
|
92
|
+
filter_complex[filter_complex.count-1] += ' [overlayed]'
|
93
|
+
filter_complex << '[overlayed] ' + rotate(output.rotate)
|
94
|
+
end
|
95
|
+
output.options[:filter_complex] = "'#{filter_complex.join(';')}'"
|
96
|
+
else
|
97
|
+
filter_complex << "overlay=#{overlay(output.watermarks[:x], :w)}:#{overlay(output.watermarks[:y], :h)}" if output.watermarks
|
98
|
+
filter_complex << rotate(output.rotate) if output.rotate
|
99
|
+
output.options[:filter_complex] = filter_complex.join(',') if filter_complex.any?
|
90
100
|
end
|
91
|
-
output.options[:filter_complex] = "'#{filter_complex.join(';')}'"
|
92
101
|
else
|
93
|
-
|
94
|
-
|
95
|
-
output.options[:filter_complex] = filter_complex.join(',') if filter_complex.any?
|
102
|
+
output.options.delete(:deinterlace)
|
103
|
+
output.options.delete(:filter_complex)
|
96
104
|
end
|
97
|
-
|
98
105
|
output.options[:format] ||= File.extname(output.filename).delete('.')
|
106
|
+
output.options[:format] = 'mpegts' if output.options[:format] == 'ts'
|
107
|
+
output.options[:movflags] = '+faststart' if output.faststart || (output.faststart.nil? && %w(mov mp4).include?(output.options[:format].downcase))
|
99
108
|
output.options = {
|
100
109
|
:threads => 1,
|
101
110
|
:video_codec => 'libx264',
|
@@ -126,7 +135,7 @@ module VideoConverter
|
|
126
135
|
# TODO compare by size
|
127
136
|
res
|
128
137
|
end.last
|
129
|
-
success &&= Command.new(self.class.first_pass_command, self.class.prepare_params(input, best_quality)).execute
|
138
|
+
success &&= Command.new(self.class.first_pass_command, self.class.prepare_params(input, best_quality), ['-filter_complex']).execute
|
130
139
|
end
|
131
140
|
|
132
141
|
qualities.each_with_index do |output, output_index|
|
@@ -136,12 +145,12 @@ module VideoConverter
|
|
136
145
|
self.class.second_pass_command
|
137
146
|
else
|
138
147
|
output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
|
139
|
-
output.options[:force_key_frames] = (input.metadata[:duration_in_ms] / 1000.0
|
148
|
+
output.options[:force_key_frames] = input.metadata[:video_start_time].step(input.metadata[:duration_in_ms] / 1000.0, Output.keyframe_interval_in_seconds).map(&:floor).join(',')
|
140
149
|
Command.chain(self.class.first_pass_command, self.class.second_pass_command)
|
141
150
|
end
|
142
151
|
|
143
152
|
# run ffmpeg
|
144
|
-
command = Command.new(command, self.class.prepare_params(input, output))
|
153
|
+
command = Command.new(command, self.class.prepare_params(input, output), ['-filter_complex'])
|
145
154
|
if VideoConverter.paral
|
146
155
|
threads << Thread.new { success &&= command.execute }
|
147
156
|
else
|
@@ -158,14 +167,14 @@ module VideoConverter
|
|
158
167
|
def self.concat_muxer(inputs, output)
|
159
168
|
list = File.join(output.work_dir, 'list.txt')
|
160
169
|
# NOTE ffmpeg concat list requires unescaped files
|
161
|
-
File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.
|
170
|
+
File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.to_s)}'" }.join("\n"))
|
162
171
|
success = Command.new(concat_command, prepare_params(list, output)).execute
|
163
172
|
FileUtils.rm list if success
|
164
173
|
success
|
165
174
|
end
|
166
175
|
|
167
176
|
def self.concat_protocol(inputs, output)
|
168
|
-
Command.new(one_pass_command, prepare_params('"concat:' + inputs.join('|') + '"', output)).execute
|
177
|
+
Command.new(one_pass_command, prepare_params('"concat:' + inputs.map { |input| input.to_s.shellescape }.join('|') + '"', output), [:inputs]).execute
|
169
178
|
end
|
170
179
|
|
171
180
|
def common_first_pass?(qualities)
|
@@ -181,16 +190,10 @@ module VideoConverter
|
|
181
190
|
|
182
191
|
{
|
183
192
|
:bin => bin,
|
184
|
-
:
|
185
|
-
:
|
186
|
-
|
187
|
-
|
188
|
-
option = '-' + (aliases[option] || option).to_s
|
189
|
-
Array.wrap(values).map do |value|
|
190
|
-
value == true ? option : "#{option} #{value}"
|
191
|
-
end.join(' ')
|
192
|
-
end
|
193
|
-
end.compact.join(' '),
|
193
|
+
:inputs => { '-i' => output.watermarks ? [input.to_s, output.watermarks[:url]] : input.to_s },
|
194
|
+
:options => Hash[*output.options.select { |option, values| !output.respond_to?(option) }.map do |option, values|
|
195
|
+
['-' + (aliases[option] || option).to_s, values]
|
196
|
+
end.flatten(1)],
|
194
197
|
:output => output.ffmpeg_output,
|
195
198
|
:log => output.log
|
196
199
|
}
|
@@ -200,7 +203,7 @@ module VideoConverter
|
|
200
203
|
if size.to_s.end_with?('%')
|
201
204
|
percent_of ? (percent_of * size.to_f / 100).to_i : "i#{wh}*#{size.to_f/100}"
|
202
205
|
else
|
203
|
-
size || "trunc\\(o#{{:h =>
|
206
|
+
size || "trunc\\(o#{{:h => 'w/', :w => 'h*'}[wh]}a/2\\)*2"
|
204
207
|
end
|
205
208
|
end
|
206
209
|
|
@@ -233,8 +236,15 @@ module VideoConverter
|
|
233
236
|
Float(aspect)
|
234
237
|
end
|
235
238
|
else
|
236
|
-
|
239
|
+
width_smaller_in = output.crop ? input.video_stream[:width].to_f / crop_parse(output.crop)[:width].to_f : 1
|
240
|
+
height_smaller_in = output.crop ? input.video_stream[:height].to_f / crop_parse(output.crop)[:height].to_f : 1
|
241
|
+
((input.video_stream[:dar_width] || input.video_stream[:width]).to_f / width_smaller_in) /
|
242
|
+
((input.video_stream[:dar_height] || input.video_stream[:height]).to_f / height_smaller_in)
|
237
243
|
end
|
238
244
|
end
|
245
|
+
|
246
|
+
def crop_parse(crop)
|
247
|
+
crop.match(/^(?:w=)?(?<width>\d+):(?:h=)?(?<height>\d+)(?::(?:x=)?(?<h>\d+):(?:y=)?(?<y>\d+))?$/) or raise 'Unsupported crop format'
|
248
|
+
end
|
239
249
|
end
|
240
250
|
end
|
data/lib/video_converter/hash.rb
CHANGED
@@ -12,13 +12,15 @@ class Hash
|
|
12
12
|
self.replace(self.deep_symbolize_keys)
|
13
13
|
end
|
14
14
|
|
15
|
-
def deep_shellescape_values
|
15
|
+
def deep_shellescape_values(safe_keys = [])
|
16
16
|
inject({}) do |options, (key, value)|
|
17
|
-
if
|
18
|
-
value
|
17
|
+
if safe_keys.include?(key)
|
18
|
+
value
|
19
|
+
elsif value.is_a? Array
|
20
|
+
value = value.map { |v| v.is_a?(Hash) ? v.deep_shellescape_values(safe_keys) : v.shellescape }
|
19
21
|
elsif value.is_a? Hash
|
20
|
-
value = value.deep_shellescape_values
|
21
|
-
elsif value.is_a?
|
22
|
+
value = value.deep_shellescape_values(safe_keys)
|
23
|
+
elsif value.is_a?(String) && !value.empty?
|
22
24
|
value = value.shellescape
|
23
25
|
end
|
24
26
|
options[key] = value
|
@@ -26,7 +28,28 @@ class Hash
|
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
def deep_shellescape_values!
|
30
|
-
self.replace(self.deep_shellescape_values)
|
31
|
-
end
|
31
|
+
def deep_shellescape_values!(safe_keys = [])
|
32
|
+
self.replace(self.deep_shellescape_values(safe_keys))
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_join(separator)
|
36
|
+
map do |key, value|
|
37
|
+
case value.class.to_s
|
38
|
+
when 'TrueClass'
|
39
|
+
key
|
40
|
+
when 'FalseClass', 'NilClass'
|
41
|
+
nil
|
42
|
+
when 'Array'
|
43
|
+
value.map { |v| "#{key} #{v}"}
|
44
|
+
when 'Hash'
|
45
|
+
value.deep_join(separator)
|
46
|
+
else
|
47
|
+
"#{key} #{value}"
|
48
|
+
end
|
49
|
+
end.join(separator)
|
50
|
+
end
|
51
|
+
|
52
|
+
def deep_join!(separator)
|
53
|
+
self.replace(self.deep_join(separator))
|
54
|
+
end
|
32
55
|
end
|
@@ -21,10 +21,6 @@ module VideoConverter
|
|
21
21
|
input
|
22
22
|
end
|
23
23
|
|
24
|
-
def unescape
|
25
|
-
input.gsub(/\\+([^n])/, '\1')
|
26
|
-
end
|
27
|
-
|
28
24
|
def metadata
|
29
25
|
unless @metadata
|
30
26
|
@metadata = {}
|
@@ -74,6 +70,19 @@ module VideoConverter
|
|
74
70
|
@mean_volume ||= Command.new(Ffmpeg.volume_detect_command, :bin => Ffmpeg.bin, :input => input).capture.match(/mean_volume:\s([-\d.]+)\sdB/).to_a[1]
|
75
71
|
end
|
76
72
|
|
73
|
+
def crop_detect(samples = 5)
|
74
|
+
(@crop_detect ||= samples.times.map do |sample|
|
75
|
+
(metadata[:duration_in_ms] / (samples + 1) / 1000.0 * (sample + 1)).round
|
76
|
+
end.uniq.map do |ss|
|
77
|
+
Command.new(Ffmpeg.crop_detect_command, :bin => Ffmpeg.bin, :ss => ss, :input => input, :vframes => 2).capture
|
78
|
+
.match(/Parsed_cropdetect.+crop=(?<crop>(?<w>[-\d]+):(?<h>[-\d]+):(?<x>\d+):(?<y>\d+))/)
|
79
|
+
end.compact.max do |m1, m2|
|
80
|
+
res = m1[:h].to_i <=> m2[:h].to_i
|
81
|
+
res = m1[:w].to_i <=> m2[:w].to_i if res == 0
|
82
|
+
res
|
83
|
+
end || {})[:crop]
|
84
|
+
end
|
85
|
+
|
77
86
|
def select_outputs(outputs)
|
78
87
|
outputs.select { |output| !output.path || output.path == input }
|
79
88
|
end
|
@@ -111,8 +120,7 @@ module VideoConverter
|
|
111
120
|
end
|
112
121
|
|
113
122
|
def is_local?
|
114
|
-
|
115
|
-
File.file?(input.gsub('\\', ''))
|
123
|
+
File.file?(input)
|
116
124
|
end
|
117
125
|
|
118
126
|
end
|
@@ -35,7 +35,7 @@ module VideoConverter
|
|
35
35
|
res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{stream[:bandwidth].to_i * 1000}\n"
|
36
36
|
res += stream[:path] + "\n"
|
37
37
|
end
|
38
|
-
res += "#EXT-X-ENDLIST"
|
38
|
+
res += "#EXT-X-ENDLIST\n"
|
39
39
|
File.open(File.join(playlist.work_dir, playlist.filename), 'w') { |f| f.write res }
|
40
40
|
true
|
41
41
|
end
|
@@ -27,7 +27,7 @@ module VideoConverter
|
|
27
27
|
{
|
28
28
|
:work_dir => outputs.select { |output| output.type != 'playlist' }.first.work_dir,
|
29
29
|
:bin => bin,
|
30
|
-
:inputs => outputs.select { |output| output.type != 'playlist' }.map { |input| "
|
30
|
+
:inputs => { '--src' => outputs.select { |output| output.type != 'playlist' }.map { |input| "#{File.basename(input.ffmpeg_output)}" } },
|
31
31
|
:manifest => outputs.detect { |output| output.type == 'playlist' }.ffmpeg_output,
|
32
32
|
:log => outputs.first.log
|
33
33
|
}
|
@@ -28,7 +28,7 @@ module VideoConverter
|
|
28
28
|
|
29
29
|
def self.get_encryption_key_path(output)
|
30
30
|
if output.encryption_key
|
31
|
-
File.open(File.join(output.work_dir,'video.key'), 'wb') { |f| f.
|
31
|
+
File.open(File.join(output.work_dir,'video.key'), 'wb') { |f| f.write output.encryption_key }
|
32
32
|
'video.key'
|
33
33
|
elsif output.encryption_key_url
|
34
34
|
uri = URI(output.encryption_key_url)
|
Binary file
|
Binary file
|
@@ -61,6 +61,40 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
61
61
|
assert_equal 240, m[:height].to_i
|
62
62
|
end
|
63
63
|
end
|
64
|
+
|
65
|
+
context 'with watermarks' do
|
66
|
+
setup do
|
67
|
+
(@c = VideoConverter.new(
|
68
|
+
:input => 'test/fixtures/test (1).mp4',
|
69
|
+
:outputs => [
|
70
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :watermarks => {
|
71
|
+
:url => 'test/fixtures/logo.png', :x=>'-3%', :y=>'-3%', :height=>'3%'
|
72
|
+
}, :height => 240 },
|
73
|
+
]
|
74
|
+
)).run
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'be ok' do
|
78
|
+
assert File.exists?(@c.outputs.first.ffmpeg_output)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'with autocrop' do
|
83
|
+
setup do
|
84
|
+
(@c = VideoConverter.new(
|
85
|
+
:input => 'test/fixtures/test_crop.mp4',
|
86
|
+
:outputs => [
|
87
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :crop => true },
|
88
|
+
]
|
89
|
+
)).run
|
90
|
+
end
|
91
|
+
|
92
|
+
should 'crop' do
|
93
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.video_stream
|
94
|
+
assert_equal 406, m[:height].to_i
|
95
|
+
assert_equal 720, m[:width].to_i
|
96
|
+
end
|
97
|
+
end
|
64
98
|
end
|
65
99
|
|
66
100
|
context 'segmentation' do
|
@@ -154,8 +188,8 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
154
188
|
end
|
155
189
|
|
156
190
|
assert_equal(
|
157
|
-
(k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :
|
158
|
-
(k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :
|
191
|
+
(k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :inputs => File.join(@c.outputs.first.work_dir, 'sd1.mp4')).capture),
|
192
|
+
(k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :inputs => File.join(@c.outputs.first.work_dir, 'sd2.mp4')).capture)
|
159
193
|
)
|
160
194
|
end
|
161
195
|
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.
|
4
|
+
version: 0.9.0
|
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:
|
12
|
+
date: 2015-03-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: video_screenshoter
|
@@ -110,7 +110,6 @@ files:
|
|
110
110
|
- lib/video_converter/base.rb
|
111
111
|
- lib/video_converter/command.rb
|
112
112
|
- lib/video_converter/f4fpackager.rb
|
113
|
-
- lib/video_converter/faststart.rb
|
114
113
|
- lib/video_converter/ffmpeg.rb
|
115
114
|
- lib/video_converter/hash.rb
|
116
115
|
- lib/video_converter/input.rb
|
@@ -122,7 +121,9 @@ files:
|
|
122
121
|
- lib/video_converter/version.rb
|
123
122
|
- test/fixtures/chunk0.ts
|
124
123
|
- test/fixtures/chunk1.ts
|
124
|
+
- test/fixtures/logo.png
|
125
125
|
- test/fixtures/test (1).mp4
|
126
|
+
- test/fixtures/test_crop.mp4
|
126
127
|
- test/fixtures/test_playlist.m3u8
|
127
128
|
- test/test_helper.rb
|
128
129
|
- test/video_converter_test.rb
|
@@ -142,7 +143,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
143
|
version: '0'
|
143
144
|
segments:
|
144
145
|
- 0
|
145
|
-
hash:
|
146
|
+
hash: -163534295853469513
|
146
147
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
148
|
none: false
|
148
149
|
requirements:
|
@@ -151,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
152
|
version: '0'
|
152
153
|
segments:
|
153
154
|
- 0
|
154
|
-
hash:
|
155
|
+
hash: -163534295853469513
|
155
156
|
requirements:
|
156
157
|
- ffmpeg, version 1.2 or greated configured with libx264 and libfaac
|
157
158
|
- live_segmenter to convert to hls
|
@@ -163,7 +164,9 @@ summary: Ffmpeg, mencoder based converter to mp4, m3u8
|
|
163
164
|
test_files:
|
164
165
|
- test/fixtures/chunk0.ts
|
165
166
|
- test/fixtures/chunk1.ts
|
167
|
+
- test/fixtures/logo.png
|
166
168
|
- test/fixtures/test (1).mp4
|
169
|
+
- test/fixtures/test_crop.mp4
|
167
170
|
- test/fixtures/test_playlist.m3u8
|
168
171
|
- test/test_helper.rb
|
169
172
|
- test/video_converter_test.rb
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
|
3
|
-
module VideoConverter
|
4
|
-
class Faststart
|
5
|
-
class << self
|
6
|
-
attr_accessor :bin, :command
|
7
|
-
end
|
8
|
-
self.bin = '/usr/local/bin/qt-faststart'
|
9
|
-
self.command = '%{bin} %{input} %{moov_atom} && mv %{moov_atom} %{input} 1>>%{log} 2>&1 || exit 1'
|
10
|
-
|
11
|
-
attr_accessor :output
|
12
|
-
|
13
|
-
def initialize output
|
14
|
-
self.output = output
|
15
|
-
end
|
16
|
-
|
17
|
-
def run
|
18
|
-
success = true
|
19
|
-
success &&= Command.new(self.class.command, prepare_params(output)).execute
|
20
|
-
success
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def prepare_params(output)
|
26
|
-
{
|
27
|
-
:bin => self.class.bin,
|
28
|
-
:log => output.log,
|
29
|
-
:input => output.ffmpeg_output,
|
30
|
-
:moov_atom => "#{output.ffmpeg_output}.mov"
|
31
|
-
}
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
end
|