video_converter 0.9.1 → 0.10.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.
- 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
|