video_converter 0.8.1 → 0.8.2
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.
@@ -3,21 +3,17 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class LiveSegmenter
|
5
5
|
class << self
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :chunks_pattern, :command
|
7
7
|
end
|
8
8
|
|
9
|
-
self.
|
10
|
-
self.
|
11
|
-
self.encoding_profile = 's'
|
12
|
-
self.select_streams = 'v'
|
13
|
-
|
14
|
-
self.command = '%{ffmpeg_bin} -i %{ffmpeg_output} -c:v copy -c:a copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{keyframe_interval_in_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
|
9
|
+
self.chunks_pattern = 's-%05d.ts'
|
10
|
+
self.command = '%{ffmpeg_bin} -i %{ffmpeg_output} -c copy -map 0 -f ssegment -segment_time %{segment_time} -segment_list %{segment_list} -segment_list_entry_prefix %{segment_list_entry_prefix} %{chunks} 1>>%{log} 2>&1'
|
15
11
|
|
16
12
|
def self.run(outputs)
|
17
13
|
success = true
|
18
14
|
threads = []
|
19
15
|
p = Proc.new do |output|
|
20
|
-
|
16
|
+
Command.new(command, prepare_params(output)).execute
|
21
17
|
end
|
22
18
|
outputs.select { |output| output.type != 'playlist' }.each do |output|
|
23
19
|
if VideoConverter.paral
|
@@ -33,37 +29,6 @@ module VideoConverter
|
|
33
29
|
|
34
30
|
private
|
35
31
|
|
36
|
-
def self.make_chunks output
|
37
|
-
Command.new(command, prepare_params(output)).execute
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.gen_quality_playlist output
|
41
|
-
res = ''
|
42
|
-
durations = []
|
43
|
-
# order desc
|
44
|
-
chunks = Dir::glob(File.join(output.chunks_dir, "#{chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
|
45
|
-
File.basename(c2).match(/\d+/).to_s.to_i <=> File.basename(c1).match(/\d+/).to_s.to_i
|
46
|
-
end
|
47
|
-
# chunk duration = (pts of first frame of the next chunk - pts of first frame of current chunk) / time_base
|
48
|
-
# for the last chunks the last two pts are used
|
49
|
-
prl_pts, l_pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunks.first} | tail -n2 2>&1`.split("\n").map { |l| l.split(',')[3].to_i }
|
50
|
-
# NOTE for case when chunk has one frame
|
51
|
-
l_pts ||= prl_pts
|
52
|
-
next_chunk_pts = 2 * l_pts - prl_pts
|
53
|
-
time_base = `#{Ffmpeg.ffprobe_bin} -show_streams -select_streams #{select_streams} -loglevel fatal #{chunks.first} 2>&1`.match(/\ntime_base=1\/(\d+)/)[1].to_f
|
54
|
-
chunks.each do |chunk|
|
55
|
-
pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunk} | head -n1 2>&1`.split(',')[3].to_i
|
56
|
-
durations << (duration = (next_chunk_pts - pts) / time_base)
|
57
|
-
next_chunk_pts = pts
|
58
|
-
res = File.join(File.basename(output.chunks_dir), File.basename(chunk)) + "\n" + res
|
59
|
-
res = "#EXTINF:%0.2f,\n" % duration + res
|
60
|
-
end
|
61
|
-
encryption_info = OpenSSL.get_encryption_key_path(output)
|
62
|
-
encryption_info = "#EXT-X-KEY:METHOD=AES-128,URI=\"#{encryption_info}\"\n" if encryption_info
|
63
|
-
res = "#EXTM3U\n#{encryption_info}#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max.to_f.ceil}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
|
64
|
-
!!File.open(File.join(output.work_dir, output.filename), 'w') { |f| f.write res }
|
65
|
-
end
|
66
|
-
|
67
32
|
def self.gen_group_playlist playlist
|
68
33
|
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n"
|
69
34
|
playlist.streams.sort { |s1, s2| s1[:bandwidth].to_i <=> s2[:bandwidth].to_i }.each do |stream|
|
@@ -79,11 +44,10 @@ module VideoConverter
|
|
79
44
|
{
|
80
45
|
:ffmpeg_bin => Ffmpeg.bin,
|
81
46
|
:ffmpeg_output => output.ffmpeg_output,
|
82
|
-
:
|
83
|
-
:
|
84
|
-
:
|
85
|
-
:
|
86
|
-
:encoding_profile => encoding_profile,
|
47
|
+
:segment_time => Output.keyframe_interval_in_seconds,
|
48
|
+
:segment_list => File.join(output.work_dir, output.filename),
|
49
|
+
:segment_list_entry_prefix => File.basename(output.chunks_dir) + '/',
|
50
|
+
:chunks => File.join(output.chunks_dir, chunks_pattern),
|
87
51
|
:log => output.log
|
88
52
|
}
|
89
53
|
end
|
@@ -11,8 +11,10 @@ module VideoConverter
|
|
11
11
|
|
12
12
|
def self.run(output)
|
13
13
|
success = true
|
14
|
+
playlist = File.join(output.work_dir, output.filename)
|
15
|
+
File.write(playlist, File.read(playlist).sub("#EXTM3U\n", "#EXTM3U\n#EXT-X-KEY:METHOD=AES-128,URI=\"#{OpenSSL.get_encryption_key_path(output)}\"\n"))
|
14
16
|
encryption_dir = FileUtils.mkdir_p("#{output.chunks_dir}_encrypted").first
|
15
|
-
chunks = Dir::glob(File.join(output.chunks_dir, "
|
17
|
+
chunks = Dir::glob(File.join(output.chunks_dir, "*.ts")).sort do |c1, c2|
|
16
18
|
File.basename(c1).match(/\d+/).to_s.to_i <=> File.basename(c2).match(/\d+/).to_s.to_i
|
17
19
|
end
|
18
20
|
chunks.each_with_index do |chunk, index|
|
@@ -88,7 +88,7 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
88
88
|
should 'generate hls' do
|
89
89
|
%w(sd1 sd2 hd1 hd2).each do |quality|
|
90
90
|
# should create chunks
|
91
|
-
assert_equal ['s-
|
91
|
+
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
92
92
|
# TODO verify that chunks have different quality (weight)
|
93
93
|
# should create playlists
|
94
94
|
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
@@ -97,6 +97,37 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
100
|
+
context 'to HLS AES' do
|
101
|
+
setup do
|
102
|
+
(@c = VideoConverter.new(
|
103
|
+
"input"=>["test/fixtures/test (1).mp4"],
|
104
|
+
"output"=>[
|
105
|
+
{"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
106
|
+
{"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528, "drm"=>"hls", "encryption_key"=>'a'*16},
|
107
|
+
{"filename"=>"playlist.m3u8", "type"=>"playlist", "streams"=>[
|
108
|
+
{"path"=>"sd1.m3u8", "bandwidth"=>804}, {"path"=>"sd2.m3u8", "bandwidth"=>1300}
|
109
|
+
]},
|
110
|
+
{"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
111
|
+
{"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720, "drm"=>"hls", "encryption_key"=>'a'*16},
|
112
|
+
{"filename"=>"hd_playlist.m3u8", "type"=>"playlist", "streams"=>[
|
113
|
+
{"path"=>"hd1.m3u8", "bandwidth"=>1598}, {"path"=>"hd2.m3u8", "bandwidth"=>3328}
|
114
|
+
]}
|
115
|
+
]
|
116
|
+
)).run
|
117
|
+
end
|
118
|
+
|
119
|
+
should 'generate hls' do
|
120
|
+
%w(sd1 sd2 hd1 hd2).each do |quality|
|
121
|
+
# should create chunks
|
122
|
+
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
123
|
+
# should create playlists
|
124
|
+
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
125
|
+
assert File.read(playlist).include?('EXT-X-KEY:METHOD=AES-128,URI="video.key"')
|
126
|
+
assert File.exists?(File.join(@c.outputs.first.work_dir, 'video.key'))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
100
131
|
context 'to HDS' do
|
101
132
|
setup do
|
102
133
|
VideoConverter.paral = true
|
@@ -150,7 +181,7 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
150
181
|
should 'generate hls' do
|
151
182
|
%w(q1 q2).each do |quality|
|
152
183
|
# should create chunks
|
153
|
-
assert_equal ['s-
|
184
|
+
assert_equal ['s-00000.ts', 's-00001.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
|
154
185
|
# TODO verify that chunks have the same quality (weight)
|
155
186
|
# should create playlists
|
156
187
|
assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
|
@@ -189,22 +220,4 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
189
220
|
)
|
190
221
|
end
|
191
222
|
end
|
192
|
-
|
193
|
-
# context 'muxing' do
|
194
|
-
# setup do
|
195
|
-
# input = "test/fixtures/test (1).mp4"
|
196
|
-
# (c1 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:0', :filename => 'video.mp4' })).convert
|
197
|
-
# (c2 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:1', :filename => 'audio.wav' })).convert
|
198
|
-
# (@c = VideoConverter.new(:uid => 'test', :input => [c1.outputs.first.ffmpeg_output, c2.outputs.first.ffmpeg_output], :output => { :filename => 'mux.mp4' })).mux
|
199
|
-
# @metadata = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.metadata
|
200
|
-
# end
|
201
|
-
# should 'mux streams' do
|
202
|
-
# assert File.exists?(@c.outputs.first.ffmpeg_output)
|
203
|
-
# assert_equal '0:0', @metadata[:video_stream]
|
204
|
-
# assert_equal '0:1', @metadata[:audio_stream]
|
205
|
-
# end
|
206
|
-
# teardown do
|
207
|
-
# #FileUtils.rm_r @c.outputs.first.work_dir
|
208
|
-
# end
|
209
|
-
# end
|
210
223
|
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.8.
|
4
|
+
version: 0.8.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-11-
|
12
|
+
date: 2014-11-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: video_screenshoter
|
@@ -142,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
142
|
version: '0'
|
143
143
|
segments:
|
144
144
|
- 0
|
145
|
-
hash:
|
145
|
+
hash: 25263521592852440
|
146
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
147
|
none: false
|
148
148
|
requirements:
|
@@ -151,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
151
|
version: '0'
|
152
152
|
segments:
|
153
153
|
- 0
|
154
|
-
hash:
|
154
|
+
hash: 25263521592852440
|
155
155
|
requirements:
|
156
156
|
- ffmpeg, version 1.2 or greated configured with libx264 and libfaac
|
157
157
|
- live_segmenter to convert to hls
|