video_converter 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/lib/video_converter.rb +2 -0
- data/lib/video_converter/command.rb +8 -5
- data/lib/video_converter/ffmpeg.rb +18 -16
- data/lib/video_converter/input.rb +15 -3
- data/lib/video_converter/openssl.rb +3 -2
- data/lib/video_converter/version.rb +1 -1
- data/test/fixtures/test_no_sound (1).mp4 +0 -0
- data/test/video_converter_test.rb +110 -41
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16c106529bdbd5b91396f8864f23d01ebb6ab2f1
|
4
|
+
data.tar.gz: 8bfcbdd50a8641a38b52096223a33a4015b752d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b039f0550490cf96c4db35927cd138a4a839224b23b04aa1aa69dea55be3ee13b3fd335b8ec9b40b301fabfbaf919dcff86718d2b5c2b02cbcc9bde820e96245
|
7
|
+
data.tar.gz: 4d03de51303d79eded64eeffa7421a1916213ee20eb11f72e7070e5f421db492713e5c50af11ca7b99d0bb9a3d15845938bbff9a7d39a0bd200310cf8cd3f8a6
|
data/.gitignore
CHANGED
data/lib/video_converter.rb
CHANGED
@@ -3,15 +3,11 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Command
|
5
5
|
class << self
|
6
|
-
attr_accessor :dry_run, :verbose
|
6
|
+
attr_accessor :dry_run, :verbose, :nice, :ionice
|
7
7
|
end
|
8
8
|
self.dry_run = false
|
9
9
|
self.verbose = true
|
10
10
|
|
11
|
-
def self.chain(*commands)
|
12
|
-
commands.map { |c| "(#{c})" }.join(' && ')
|
13
|
-
end
|
14
|
-
|
15
11
|
attr_accessor :command
|
16
12
|
|
17
13
|
def initialize command, params = {}, safe_keys = []
|
@@ -28,6 +24,8 @@ module VideoConverter
|
|
28
24
|
end
|
29
25
|
end
|
30
26
|
raise ArgumentError.new("Command is not parsed '#{self.command}'") if self.command.match(/%{[\w\-.]+}/)
|
27
|
+
self.command = "nice -n #{self.class.nice} #{self.command}" if self.class.nice
|
28
|
+
self.command = "ionice -c 2 -n #{self.class.ionice} #{self.command}" if self.class.ionice
|
31
29
|
end
|
32
30
|
|
33
31
|
def execute params = {}
|
@@ -47,5 +45,10 @@ module VideoConverter
|
|
47
45
|
def to_s
|
48
46
|
command
|
49
47
|
end
|
48
|
+
|
49
|
+
def append(*commands)
|
50
|
+
self.command = commands.unshift(command).map { |c| "(#{c})" }.join(' && ')
|
51
|
+
self
|
52
|
+
end
|
50
53
|
end
|
51
54
|
end
|
@@ -4,7 +4,7 @@ module VideoConverter
|
|
4
4
|
class Ffmpeg
|
5
5
|
class << self
|
6
6
|
attr_accessor :aliases, :defaults, :bin, :ffprobe_bin
|
7
|
-
attr_accessor :one_pass_command, :first_pass_command, :second_pass_command, :
|
7
|
+
attr_accessor :one_pass_command, :first_pass_command, :second_pass_command, :split_command, :concat_command, :mux_command, :volume_detect_command, :crop_detect_command, :key_frames_command
|
8
8
|
end
|
9
9
|
|
10
10
|
self.aliases = {
|
@@ -22,12 +22,12 @@ module VideoConverter
|
|
22
22
|
:audio_filter => 'af',
|
23
23
|
:profile => 'vprofile'
|
24
24
|
}
|
25
|
-
self.defaults = {
|
25
|
+
self.defaults = {
|
26
26
|
:threads => 1,
|
27
27
|
:video_codec => 'libx264',
|
28
28
|
:audio_codec => 'libfaac',
|
29
29
|
:pixel_format => 'yuv420p',
|
30
|
-
:frame_rate =>
|
30
|
+
:frame_rate => 25,
|
31
31
|
:preset => 'medium',
|
32
32
|
:profile => 'main',
|
33
33
|
:level => 31,
|
@@ -36,22 +36,22 @@ module VideoConverter
|
|
36
36
|
}
|
37
37
|
self.bin = '/usr/local/bin/ffmpeg'
|
38
38
|
self.ffprobe_bin = '/usr/local/bin/ffprobe'
|
39
|
-
|
39
|
+
|
40
40
|
self.one_pass_command = '%{bin} %{inputs} -y %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
41
41
|
self.first_pass_command = '%{bin} %{inputs} -y -pass 1 -an %{options} /dev/null 1>>%{log} 2>&1 || exit 1'
|
42
42
|
self.second_pass_command = '%{bin} %{inputs} -y -pass 2 %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
43
|
-
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/,$//\''
|
44
43
|
self.split_command = '%{bin} -fflags +genpts %{inputs} %{options} %{output} 1>>%{log} 2>&1 || exit 1'
|
45
44
|
self.concat_command = "%{bin} -f concat %{inputs} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
46
45
|
self.mux_command = "%{bin} %{inputs} %{maps} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
|
47
46
|
self.volume_detect_command = "%{bin} -i %{input} -af volumedetect -c:v copy -f null - 2>&1"
|
48
47
|
self.crop_detect_command = "%{bin} -ss %{ss} -i %{input} -vframes %{vframes} -vf cropdetect=round=2 -c:a copy -f null - 2>&1"
|
48
|
+
self.key_frames_command = "%{bin} -i %{input} -an -vf \"select=eq(pict_type\\,PICT_TYPE_I),showinfo\" -f null - 2>&1"
|
49
49
|
|
50
50
|
def self.split(input, output)
|
51
51
|
output.options = { :format => 'segment', :map => 0, :codec => 'copy' }.merge(output.options)
|
52
52
|
Command.new(split_command, prepare_params(input, output)).execute
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def self.concat(inputs, output, method = nil)
|
56
56
|
method = %w(ts mpg mpeg).include?(File.extname(inputs.first.to_s).delete('.')) ? :protocol : :muxer unless method
|
57
57
|
output.options = { :codec => 'copy' }.merge(output.options)
|
@@ -65,11 +65,11 @@ module VideoConverter
|
|
65
65
|
:maps => { '-map' => inputs.each_with_index.map { |_,i| "#{i}:0" }.join(' ') }
|
66
66
|
})).execute
|
67
67
|
end
|
68
|
-
|
68
|
+
|
69
69
|
attr_accessor :input, :outputs
|
70
70
|
|
71
71
|
def initialize input, outputs
|
72
|
-
self.input = input
|
72
|
+
self.input = input
|
73
73
|
self.outputs = input.select_outputs(outputs)
|
74
74
|
|
75
75
|
self.outputs.each do |output|
|
@@ -135,7 +135,7 @@ module VideoConverter
|
|
135
135
|
|
136
136
|
# common first pass
|
137
137
|
if !one_pass?(qualities) && common_first_pass?(qualities)
|
138
|
-
qualities.each do |output|
|
138
|
+
qualities.each do |output|
|
139
139
|
output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}.log")
|
140
140
|
end
|
141
141
|
best_quality = qualities.sort do |q1, q2|
|
@@ -150,18 +150,20 @@ module VideoConverter
|
|
150
150
|
|
151
151
|
qualities.each_with_index do |output, output_index|
|
152
152
|
command = if one_pass?(qualities)
|
153
|
-
self.class.one_pass_command
|
153
|
+
Command.new(self.class.one_pass_command, self.class.prepare_params(input, output), ['-filter_complex'])
|
154
154
|
elsif common_first_pass?(qualities)
|
155
|
-
self.class.second_pass_command
|
155
|
+
Command.new(self.class.second_pass_command, self.class.prepare_params(input, output), ['-filter_complex'])
|
156
156
|
else
|
157
157
|
output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
|
158
158
|
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(',')
|
159
|
-
output.options[:
|
160
|
-
|
159
|
+
output.options[:sc_threshold] = 0
|
160
|
+
output.options[:keyint_min] = output.options[:keyframe_interval] = nil
|
161
|
+
Command.new(self.class.first_pass_command, self.class.prepare_params(input, output), ['-filter_complex']).append(
|
162
|
+
Command.new(self.class.second_pass_command, self.class.prepare_params(input, output), ['-filter_complex'])
|
163
|
+
)
|
161
164
|
end
|
162
165
|
|
163
166
|
# run ffmpeg
|
164
|
-
command = Command.new(command, self.class.prepare_params(input, output), ['-filter_complex'])
|
165
167
|
if VideoConverter.paral
|
166
168
|
threads << Thread.new { success &&= command.execute }
|
167
169
|
else
|
@@ -173,7 +175,7 @@ module VideoConverter
|
|
173
175
|
success
|
174
176
|
end
|
175
177
|
|
176
|
-
private
|
178
|
+
private
|
177
179
|
|
178
180
|
def self.concat_muxer(inputs, output)
|
179
181
|
list = File.join(output.work_dir, 'list.txt')
|
@@ -198,7 +200,7 @@ module VideoConverter
|
|
198
200
|
end
|
199
201
|
|
200
202
|
def self.prepare_params input, output
|
201
|
-
|
203
|
+
|
202
204
|
{
|
203
205
|
:bin => bin,
|
204
206
|
:inputs => { '-i' => output.watermarks ? [input.to_s, output.watermarks[:url]] : input.to_s },
|
@@ -66,6 +66,10 @@ module VideoConverter
|
|
66
66
|
metadata[:video_streams].first
|
67
67
|
end
|
68
68
|
|
69
|
+
def audio_stream
|
70
|
+
metadata[:audio_streams].first
|
71
|
+
end
|
72
|
+
|
69
73
|
def mean_volume
|
70
74
|
@mean_volume ||= Command.new(Ffmpeg.volume_detect_command, :bin => Ffmpeg.bin, :input => input).capture.match(/mean_volume:\s([-\d.]+)\sdB/).to_a[1]
|
71
75
|
end
|
@@ -83,6 +87,14 @@ module VideoConverter
|
|
83
87
|
end || {})[:crop]
|
84
88
|
end
|
85
89
|
|
90
|
+
def key_frames
|
91
|
+
@key_frames ||= Command.new(Ffmpeg.key_frames_command, :bin => Ffmpeg.bin, :input => input).capture.split("\n").map do |l|
|
92
|
+
if m = l.match(/pts:\s*(\d+)\s*pts_time:\s*(\d+(?:\.\d+)?)/)
|
93
|
+
{ :pts => m[1].to_i, :pts_time => m[2].to_f }
|
94
|
+
end
|
95
|
+
end.compact
|
96
|
+
end
|
97
|
+
|
86
98
|
def select_outputs(outputs)
|
87
99
|
outputs.select { |output| !output.path || output.path == input }
|
88
100
|
end
|
@@ -91,11 +103,11 @@ module VideoConverter
|
|
91
103
|
# qualities with the same group param are one group
|
92
104
|
groups = Hash.new([])
|
93
105
|
outputs.each { |output| groups[output.group] += [output] if output.group.present? }
|
94
|
-
groups = groups.values
|
106
|
+
groups = groups.values
|
95
107
|
|
96
108
|
# qualities of one playlist are one group
|
97
109
|
groups += outputs.select { |output| output.type == 'playlist' }.map { |playlist| playlist.output_group(outputs) }
|
98
|
-
|
110
|
+
|
99
111
|
# other outputs are separate groups
|
100
112
|
(outputs - groups.flatten).each { |output| groups << [output] }
|
101
113
|
groups
|
@@ -118,7 +130,7 @@ module VideoConverter
|
|
118
130
|
def is_http?
|
119
131
|
!!input.match(/^http:\/\//)
|
120
132
|
end
|
121
|
-
|
133
|
+
|
122
134
|
def is_local?
|
123
135
|
File.file?(input)
|
124
136
|
end
|
@@ -35,7 +35,8 @@ module VideoConverter
|
|
35
35
|
File.open(File.join(output.work_dir,'url.video.key'), 'wb') { |f| f.puts Net::HTTP.get(uri.host, uri.path) }
|
36
36
|
output.encryption_key_url
|
37
37
|
else
|
38
|
-
|
38
|
+
File.open(File.join(output.work_dir, "#{output.filename}.key"), 'wb') { |f| f.write SecureRandom.hex(8) }
|
39
|
+
"#{output.filename}.key"
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
@@ -45,7 +46,7 @@ module VideoConverter
|
|
45
46
|
elsif output.encryption_key_url
|
46
47
|
File.join(output.work_dir, "url.video.key")
|
47
48
|
else
|
48
|
-
|
49
|
+
File.join(output.work_dir, "#{output.filename}.key")
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
Binary file
|
@@ -5,10 +5,10 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
5
5
|
context 'with thumbnails' do
|
6
6
|
setup do
|
7
7
|
(@c = VideoConverter.new(
|
8
|
-
:input => 'test/fixtures/test (1).mp4',
|
8
|
+
:input => 'test/fixtures/test (1).mp4',
|
9
9
|
:outputs => [
|
10
|
-
{ :video_bitrate => 300, :filename => 'q1.mp4' },
|
11
|
-
{ :video_bitrate => 400, :filename => 'q2.mp4', :thumbnails => {
|
10
|
+
{ :video_bitrate => 300, :filename => 'q1.mp4' },
|
11
|
+
{ :video_bitrate => 400, :filename => 'q2.mp4', :thumbnails => {
|
12
12
|
:number => 2, :offset_start => '50%', :offset_end => '20%', :presets => { :norm => '-normalize' }, :exact => true
|
13
13
|
} },
|
14
14
|
{ :video_bitrate => 500, :filename => 'q3.mp4' }
|
@@ -29,9 +29,9 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
29
29
|
context 'with aspect' do
|
30
30
|
setup do
|
31
31
|
(@c = VideoConverter.new(
|
32
|
-
:input => 'test/fixtures/test (1).mp4',
|
32
|
+
:input => 'test/fixtures/test (1).mp4',
|
33
33
|
:outputs => [
|
34
|
-
{ :video_bitrate => 300, :filename => 'res.mp4', :aspect => '4:3' },
|
34
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :aspect => '4:3' },
|
35
35
|
]
|
36
36
|
)).run
|
37
37
|
end
|
@@ -46,9 +46,9 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
46
46
|
context 'with aspect and resize' do
|
47
47
|
setup do
|
48
48
|
(@c = VideoConverter.new(
|
49
|
-
:input => 'test/fixtures/test (1).mp4',
|
49
|
+
:input => 'test/fixtures/test (1).mp4',
|
50
50
|
:outputs => [
|
51
|
-
{ :video_bitrate => 300, :filename => 'res.mp4', :aspect => '4:3', :height => 240 },
|
51
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :aspect => '4:3', :height => 240 },
|
52
52
|
]
|
53
53
|
)).run
|
54
54
|
end
|
@@ -65,11 +65,11 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
65
65
|
context 'with watermarks' do
|
66
66
|
setup do
|
67
67
|
(@c = VideoConverter.new(
|
68
|
-
:input => 'test/fixtures/test (1).mp4',
|
68
|
+
:input => 'test/fixtures/test (1).mp4',
|
69
69
|
:outputs => [
|
70
70
|
{ :video_bitrate => 300, :filename => 'res.mp4', :watermarks => {
|
71
71
|
:url => 'test/fixtures/logo.png', :x=>'-3%', :y=>'-3%', :height=>'3%'
|
72
|
-
}, :height => 240 },
|
72
|
+
}, :height => 240 },
|
73
73
|
]
|
74
74
|
)).run
|
75
75
|
end
|
@@ -82,18 +82,60 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
82
82
|
context 'with autocrop' do
|
83
83
|
setup do
|
84
84
|
(@c = VideoConverter.new(
|
85
|
-
:input => 'test/fixtures/test_crop.mp4',
|
85
|
+
:input => 'test/fixtures/test_crop.mp4',
|
86
86
|
:outputs => [
|
87
|
-
{ :
|
87
|
+
{ :filename => 'audio.mp4', :ac => 1, :ar => 44100, :vn => true, :one_pass => true, :volume => '-21dB' },
|
88
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :crop => true }
|
88
89
|
]
|
89
90
|
)).run
|
90
91
|
end
|
91
|
-
|
92
|
+
|
92
93
|
should 'crop' do
|
93
|
-
m = VideoConverter.new(:input => @c.outputs.
|
94
|
+
m = VideoConverter.new(:input => @c.outputs.last.ffmpeg_output).inputs.first.video_stream
|
94
95
|
assert_equal 406, m[:height].to_i
|
95
96
|
assert_equal 720, m[:width].to_i
|
96
97
|
end
|
98
|
+
|
99
|
+
should 'consist no video in output' do
|
100
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.video_stream
|
101
|
+
assert !m
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'with sound only' do
|
106
|
+
setup do
|
107
|
+
(@c = VideoConverter.new(
|
108
|
+
:input => 'test/fixtures/test (1).mp4',
|
109
|
+
:outputs => [
|
110
|
+
{ :filename => 'audio.mp4', :ac => 1, :ar => 44100, :vn => true, :one_pass => true, :volume => '-21dB' }
|
111
|
+
]
|
112
|
+
)).run
|
113
|
+
end
|
114
|
+
|
115
|
+
should 'consist no video' do
|
116
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.video_stream
|
117
|
+
assert !m
|
118
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.audio_stream
|
119
|
+
assert m
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'with video only' do
|
124
|
+
setup do
|
125
|
+
(@c = VideoConverter.new(
|
126
|
+
:input => 'test/fixtures/test_no_sound (1).mp4',
|
127
|
+
:outputs => [
|
128
|
+
{ :video_bitrate => 300, :filename => 'res.mp4', :crop => true }
|
129
|
+
]
|
130
|
+
)).run
|
131
|
+
end
|
132
|
+
|
133
|
+
should 'consist no video' do
|
134
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.video_stream
|
135
|
+
assert m
|
136
|
+
m = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.audio_stream
|
137
|
+
assert !m
|
138
|
+
end
|
97
139
|
end
|
98
140
|
end
|
99
141
|
|
@@ -103,15 +145,15 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
103
145
|
setup do
|
104
146
|
VideoConverter.paral = true
|
105
147
|
(@c = VideoConverter.new(
|
106
|
-
"input"=>["test/fixtures/test (1).mp4"],
|
148
|
+
"input"=>["test/fixtures/test (1).mp4"],
|
107
149
|
"output"=>[
|
108
|
-
{"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
|
109
|
-
{"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
|
150
|
+
{"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
|
151
|
+
{"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
|
110
152
|
{"filename"=>"playlist.m3u8", "type"=>"playlist", "streams"=>[
|
111
153
|
{"path"=>"sd1.m3u8", "bandwidth"=>804}, {"path"=>"sd2.m3u8", "bandwidth"=>1300}
|
112
|
-
]},
|
113
|
-
{"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720},
|
114
|
-
{"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720},
|
154
|
+
]},
|
155
|
+
{"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720},
|
156
|
+
{"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720},
|
115
157
|
{"filename"=>"hd_playlist.m3u8", "type"=>"playlist", "streams"=>[
|
116
158
|
{"path"=>"hd1.m3u8", "bandwidth"=>1598}, {"path"=>"hd2.m3u8", "bandwidth"=>3328}
|
117
159
|
]}
|
@@ -122,10 +164,10 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
122
164
|
should 'generate hls' do
|
123
165
|
%w(sd1 sd2 hd1 hd2).each do |quality|
|
124
166
|
# should create chunks
|
125
|
-
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
167
|
+
assert_equal ['s-00000.ts', 's-00001.ts', 's-00002.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
126
168
|
# TODO verify that chunks have different quality (weight)
|
127
169
|
# should create playlists
|
128
|
-
assert File.exists?(
|
170
|
+
assert File.exists?(File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
129
171
|
# TODO verify that playlist is valid (contain all chunks and modifiers)
|
130
172
|
end
|
131
173
|
end
|
@@ -134,17 +176,22 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
134
176
|
context 'to HLS AES' do
|
135
177
|
setup do
|
136
178
|
(@c = VideoConverter.new(
|
137
|
-
"input"=>["test/fixtures/test (1).mp4"],
|
179
|
+
"input"=>["test/fixtures/test (1).mp4"],
|
138
180
|
"output"=>[
|
139
|
-
{"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
140
|
-
{"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
181
|
+
{"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
182
|
+
{"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
141
183
|
{"filename"=>"playlist.m3u8", "type"=>"playlist", "streams"=>[
|
142
184
|
{"path"=>"sd1.m3u8", "bandwidth"=>804}, {"path"=>"sd2.m3u8", "bandwidth"=>1300}
|
143
|
-
]},
|
144
|
-
{"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
145
|
-
{"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
185
|
+
]},
|
186
|
+
{"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
187
|
+
{"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
146
188
|
{"filename"=>"hd_playlist.m3u8", "type"=>"playlist", "streams"=>[
|
147
189
|
{"path"=>"hd1.m3u8", "bandwidth"=>1598}, {"path"=>"hd2.m3u8", "bandwidth"=>3328}
|
190
|
+
]},
|
191
|
+
{"video_bitrate"=>676, "filename"=>"sd3.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720, "drm"=>"hls"},
|
192
|
+
{"video_bitrate"=>800, "filename"=>"sd4.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720, "drm"=>"hls"},
|
193
|
+
{"filename"=>"sd3_playlist.m3u8", "type"=>"playlist", "streams"=>[
|
194
|
+
{"path"=>"sd3.m3u8", "bandwidth"=>804}, {"path"=>"sd4.m3u8", "bandwidth"=>3328}
|
148
195
|
]}
|
149
196
|
]
|
150
197
|
)).run
|
@@ -153,28 +200,46 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
153
200
|
should 'generate hls' do
|
154
201
|
%w(sd1 sd2 hd1 hd2).each do |quality|
|
155
202
|
# should create chunks
|
156
|
-
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
203
|
+
assert_equal ['s-00000.ts', 's-00001.ts', 's-00002.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
157
204
|
# should create playlists
|
158
205
|
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
159
206
|
assert File.read(playlist).include?('EXT-X-KEY:METHOD=AES-128,URI="video.key"')
|
160
207
|
assert File.exists?(File.join(@c.outputs.first.work_dir, 'video.key'))
|
161
208
|
end
|
162
209
|
end
|
210
|
+
|
211
|
+
should 'be deencrypted successfully' do
|
212
|
+
# with encryption key
|
213
|
+
key = `hexdump -e '16/1 "%02x"' #{File.join(@c.outputs.first.work_dir, 'video.key')}`
|
214
|
+
`openssl aes-128-cbc -d -in #{File.join(@c.outputs.first.work_dir, 'sd1/s-00000.ts')} -out #{File.join(@c.outputs.first.work_dir, '1.ts')} -K #{key} -iv 00000000000000000000000000000000`
|
215
|
+
meta = VideoConverter.new(:inputs => File.join(@c.outputs.first.work_dir, '1.ts')).inputs.first.metadata
|
216
|
+
assert_equal meta[:channels], 2
|
217
|
+
assert_equal meta[:video_streams].first[:video_codec], "h264"
|
218
|
+
|
219
|
+
#w/out encryption key
|
220
|
+
key = `hexdump -e '16/1 "%02x"' #{File.join(@c.outputs.first.work_dir, 'sd3.m3u8.key')}`
|
221
|
+
`openssl aes-128-cbc -d -in #{File.join(@c.outputs.first.work_dir, 'sd3/s-00000.ts')} -out #{File.join(@c.outputs.first.work_dir, '2.ts')} -K #{key} -iv 00000000000000000000000000000000`
|
222
|
+
meta = VideoConverter.new(:inputs => File.join(@c.outputs.first.work_dir, '2.ts')).inputs.first.metadata
|
223
|
+
assert_equal meta[:channels], 2
|
224
|
+
assert_equal meta[:video_streams].first[:video_codec], "h264"
|
225
|
+
end
|
163
226
|
end
|
164
227
|
|
165
228
|
context 'to HDS' do
|
166
229
|
setup do
|
167
230
|
VideoConverter.paral = true
|
231
|
+
VideoConverter::Command.nice = 10
|
232
|
+
VideoConverter::Command.ionice = 6
|
168
233
|
(@c = VideoConverter.new(
|
169
|
-
"input"=>["test/fixtures/test (1).mp4"],
|
234
|
+
"input"=>["test/fixtures/test (1).mp4"],
|
170
235
|
"output"=>[
|
171
|
-
{"video_bitrate"=>676, "filename"=>"sd1.mp4", "audio_bitrate"=>128, "height"=>360},
|
172
|
-
{"video_bitrate"=>1172, "filename"=>"sd2.mp4", "audio_bitrate"=>128, "height"=>528},
|
236
|
+
{"video_bitrate"=>676, "filename"=>"sd1.mp4", "audio_bitrate"=>128, "height"=>360},
|
237
|
+
{"video_bitrate"=>1172, "filename"=>"sd2.mp4", "audio_bitrate"=>128, "height"=>528},
|
173
238
|
{"filename"=>"playlist.f4m", "type"=>"playlist", "streams"=>[
|
174
239
|
{"path"=>"sd1.mp4", "bandwidth"=>804}, {"path"=>"sd2.mp4", "bandwidth"=>1300}
|
175
|
-
]},
|
176
|
-
{"video_bitrate"=>1550, "filename"=>"hd1.mp4", "audio_bitrate"=>48, "height"=>720},
|
177
|
-
{"video_bitrate"=>3200, "filename"=>"hd2.mp4", "audio_bitrate"=>128, "height"=>720},
|
240
|
+
]},
|
241
|
+
{"video_bitrate"=>1550, "filename"=>"hd1.mp4", "audio_bitrate"=>48, "height"=>720},
|
242
|
+
{"video_bitrate"=>3200, "filename"=>"hd2.mp4", "audio_bitrate"=>128, "height"=>720},
|
178
243
|
{"filename"=>"hd_playlist.f4m", "type"=>"playlist", "streams"=>[
|
179
244
|
{"path"=>"hd1.mp4", "bandwidth"=>1598}, {"path"=>"hd2.mp4", "bandwidth"=>3328}
|
180
245
|
]}
|
@@ -186,12 +251,16 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
186
251
|
%w(sd1.mp4 sd2.mp4 playlist.f4m hd1.mp4 hd2.mp4 hd_playlist.f4m).each do |filename|
|
187
252
|
assert File.exists?(File.join(@c.outputs.first.work_dir, "#{filename}"))
|
188
253
|
end
|
189
|
-
|
190
254
|
assert_equal(
|
191
|
-
|
192
|
-
|
255
|
+
VideoConverter.new(:input => File.join(@c.outputs.first.work_dir, 'sd1.mp4')).inputs.first.key_frames,
|
256
|
+
VideoConverter.new(:input => File.join(@c.outputs.first.work_dir, 'sd2.mp4')).inputs.first.key_frames
|
193
257
|
)
|
194
258
|
end
|
259
|
+
|
260
|
+
teardown do
|
261
|
+
VideoConverter::Command.nice = nil
|
262
|
+
VideoConverter::Command.ionice = nil
|
263
|
+
end
|
195
264
|
end
|
196
265
|
end
|
197
266
|
|
@@ -200,10 +269,10 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
200
269
|
VideoConverter.paral = false
|
201
270
|
FileUtils.cp("test/fixtures/test (1).mp4", "test/fixtures/test (2).mp4")
|
202
271
|
(@c = VideoConverter.new(
|
203
|
-
:input => ["test/fixtures/test (1).mp4", "test/fixtures/test (2).mp4"],
|
272
|
+
:input => ["test/fixtures/test (1).mp4", "test/fixtures/test (2).mp4"],
|
204
273
|
:output => [
|
205
|
-
{:filename=>"q1.m3u8", :path=>"test/fixtures/test (1).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", 'bsf:v'=>"h264_mp4toannexb"},
|
206
|
-
{:filename=>"q2.m3u8", :path=>"test/fixtures/test (2).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", 'bsf:v'=>"h264_mp4toannexb"},
|
274
|
+
{:filename=>"q1.m3u8", :path=>"test/fixtures/test (1).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", 'bsf:v'=>"h264_mp4toannexb"},
|
275
|
+
{:filename=>"q2.m3u8", :path=>"test/fixtures/test (2).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", 'bsf:v'=>"h264_mp4toannexb"},
|
207
276
|
{:filename=>"playlist.m3u8", :type=>"playlist", :streams=>[
|
208
277
|
{:path=>"q1.m3u8", :bandwidth=>464}, {:path=>"q2.m3u8", :bandwidth=>928}
|
209
278
|
]}
|
@@ -218,7 +287,7 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
218
287
|
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
219
288
|
# TODO verify that chunks have the same quality (weight)
|
220
289
|
# should create playlists
|
221
|
-
assert File.exists?(
|
290
|
+
assert File.exists?(File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
222
291
|
# TODO verify that playlist is valid (contain all chunks and modifiers)
|
223
292
|
end
|
224
293
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: video_converter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- novikov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: video_screenshoter
|
@@ -113,6 +113,7 @@ files:
|
|
113
113
|
- test/fixtures/logo.png
|
114
114
|
- test/fixtures/test (1).mp4
|
115
115
|
- test/fixtures/test_crop.mp4
|
116
|
+
- test/fixtures/test_no_sound (1).mp4
|
116
117
|
- test/fixtures/test_playlist.m3u8
|
117
118
|
- test/test_helper.rb
|
118
119
|
- test/video_converter_test.rb
|
@@ -149,6 +150,7 @@ test_files:
|
|
149
150
|
- test/fixtures/logo.png
|
150
151
|
- test/fixtures/test (1).mp4
|
151
152
|
- test/fixtures/test_crop.mp4
|
153
|
+
- test/fixtures/test_no_sound (1).mp4
|
152
154
|
- test/fixtures/test_playlist.m3u8
|
153
155
|
- test/test_helper.rb
|
154
156
|
- test/video_converter_test.rb
|