video_converter 0.8.2 → 0.9.0
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 +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
|