video_converter 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/video_converter.rb +2 -1
- data/lib/video_converter/base.rb +5 -8
- data/lib/video_converter/ffmpeg.rb +26 -22
- data/lib/video_converter/input.rb +4 -1
- data/lib/video_converter/input_array.rb +24 -0
- data/lib/video_converter/live_segmenter.rb +1 -1
- data/lib/video_converter/output.rb +12 -5
- data/lib/video_converter/output_array.rb +0 -9
- data/lib/video_converter/version.rb +1 -1
- data/test/video_converter_test.rb +53 -4
- metadata +5 -4
data/lib/video_converter.rb
CHANGED
@@ -5,6 +5,7 @@ require "video_converter/process"
|
|
5
5
|
require "video_converter/ffmpeg"
|
6
6
|
require "video_converter/live_segmenter"
|
7
7
|
require "video_converter/input"
|
8
|
+
require "video_converter/input_array"
|
8
9
|
require "video_converter/output"
|
9
10
|
require "video_converter/output_array"
|
10
11
|
require "fileutils"
|
@@ -18,7 +19,7 @@ module VideoConverter
|
|
18
19
|
self.paral = true
|
19
20
|
|
20
21
|
def self.new params
|
21
|
-
VideoConverter::Base.new params
|
22
|
+
VideoConverter::Base.new params.deep_symbolize_keys
|
22
23
|
end
|
23
24
|
|
24
25
|
def self.find uid
|
data/lib/video_converter/base.rb
CHANGED
@@ -2,16 +2,13 @@
|
|
2
2
|
|
3
3
|
module VideoConverter
|
4
4
|
class Base
|
5
|
-
attr_accessor :
|
5
|
+
attr_accessor :input_array, :output_array, :log, :uid
|
6
6
|
|
7
7
|
def initialize params
|
8
|
-
params.deep_symbolize_keys!
|
9
|
-
raise ArgumentError.new('input is needed') if params[:input].nil? || params[:input].empty?
|
10
|
-
self.input = Input.new(params[:input])
|
11
|
-
raise ArgumentError.new('input does not exist') unless input.exists?
|
12
8
|
self.uid = params[:uid] || (Socket.gethostname + object_id.to_s)
|
13
|
-
|
14
|
-
self.
|
9
|
+
self.output_array = OutputArray.new(params[:output] || {}, uid)
|
10
|
+
self.input_array = InputArray.new(params[:input], output_array)
|
11
|
+
input_array.inputs.each { |input| raise ArgumentError.new("#{input} does not exist") unless input.exists? }
|
15
12
|
if params[:log].nil?
|
16
13
|
self.log = '/dev/null'
|
17
14
|
else
|
@@ -45,7 +42,7 @@ module VideoConverter
|
|
45
42
|
|
46
43
|
def convert
|
47
44
|
params = {}
|
48
|
-
[:
|
45
|
+
[:input_array, :output_array, :log].each do |param|
|
49
46
|
params[param] = self.send(param)
|
50
47
|
end
|
51
48
|
Ffmpeg.new(params).run
|
@@ -11,16 +11,16 @@ module VideoConverter
|
|
11
11
|
self.paral = true
|
12
12
|
self.log = '/dev/null'
|
13
13
|
|
14
|
-
self.one_pass_command = "%{bin} -i %{input} -y -acodec copy -vcodec
|
14
|
+
self.one_pass_command = "%{bin} -i %{input} -y -acodec copy -vcodec %{video_codec} -g 100 -keyint_min 50 -b:v %{video_bitrate}k -bt %{video_bitrate}k -f mp4 %{local_path} 1>%{log} 2>&1 || exit 1"
|
15
15
|
|
16
|
-
self.first_pass_command = "%{bin} -i %{input} -y -an -vcodec
|
16
|
+
self.first_pass_command = "%{bin} -i %{input} -y -an -vcodec %{video_codec} -g %{keyframe_interval} -keyint_min 25 -pass 1 -passlogfile %{passlogfile} -b:v 700k -threads %{threads} -f mp4 /dev/null 1>>%{log} 2>&1 || exit 1"
|
17
17
|
|
18
|
-
self.second_pass_command = "%{bin} -i %{input} -y -pass 2 -passlogfile %{
|
18
|
+
self.second_pass_command = "%{bin} -i %{input} -y -pass 2 -passlogfile %{passlogfile} -c:a %{audio_codec} -b:a %{audio_bitrate}k -c:v %{video_codec} -g %{keyframe_interval} -keyint_min 25 %{frame_rate} -b:v %{video_bitrate}k %{size} -threads %{threads} -f mp4 %{local_path} 1>%{log} 2>&1 || exit 1"
|
19
19
|
|
20
|
-
attr_accessor :
|
20
|
+
attr_accessor :input_array, :output_array, :one_pass, :paral, :log
|
21
21
|
|
22
22
|
def initialize params
|
23
|
-
[:
|
23
|
+
[:input_array, :output_array].each do |param|
|
24
24
|
self.send("#{param}=", params[param]) or raise ArgumentError.new("#{param} is needed")
|
25
25
|
end
|
26
26
|
[:one_pass, :paral, :log].each do |param|
|
@@ -30,33 +30,37 @@ module VideoConverter
|
|
30
30
|
|
31
31
|
def run
|
32
32
|
res = true
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
quality_command = Command.new self.class.one_pass_command, prepare_params(common_params.merge(quality.to_hash))
|
42
|
-
else
|
43
|
-
quality_command = Command.new self.class.second_pass_command, prepare_params(common_params.merge(quality.to_hash))
|
33
|
+
input_array.inputs.each do |input|
|
34
|
+
threads = []
|
35
|
+
input.output_groups.each_with_index do |group, group_number|
|
36
|
+
passlogfile = File.join(File.dirname(group.first.local_path), "#{group_number}.log")
|
37
|
+
one_pass = self.one_pass || group.first.video_codec == 'copy'
|
38
|
+
unless one_pass
|
39
|
+
first_pass_command = Command.new self.class.first_pass_command, prepare_params(common_params.merge(group.first.to_hash).merge((group.first.playlist.to_hash rescue {})).merge(:passlogfile => passlogfile, :input => input))
|
40
|
+
res &&= first_pass_command.execute
|
44
41
|
end
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
42
|
+
group.each do |quality|
|
43
|
+
if one_pass
|
44
|
+
quality_command = Command.new self.class.one_pass_command, prepare_params(common_params.merge(quality.to_hash).merge(:passlogfile => passlogfile, :input => input))
|
45
|
+
else
|
46
|
+
quality_command = Command.new self.class.second_pass_command, prepare_params(common_params.merge(quality.to_hash).merge(:passlogfile => passlogfile, :input => input))
|
47
|
+
end
|
48
|
+
if paral
|
49
|
+
threads << Thread.new { res &&= quality_command.execute }
|
50
|
+
else
|
51
|
+
res &&= quality_command.execute
|
52
|
+
end
|
49
53
|
end
|
50
54
|
end
|
55
|
+
threads.each { |t| t.join } if paral
|
51
56
|
end
|
52
|
-
threads.each { |t| t.join } if paral
|
53
57
|
res
|
54
58
|
end
|
55
59
|
|
56
60
|
private
|
57
61
|
|
58
62
|
def common_params
|
59
|
-
{ :bin => self.class.bin, :
|
63
|
+
{ :bin => self.class.bin, :log => log }
|
60
64
|
end
|
61
65
|
|
62
66
|
def prepare_params params
|
@@ -8,10 +8,13 @@ module VideoConverter
|
|
8
8
|
|
9
9
|
self.metadata_command = "%{bin} -i %{input} 2>&1"
|
10
10
|
|
11
|
-
attr_accessor :input
|
11
|
+
attr_accessor :input, :outputs, :output_groups
|
12
12
|
|
13
13
|
def initialize input
|
14
|
+
raise ArgumentError.new('input is needed') if input.nil? || input.empty?
|
14
15
|
self.input = input
|
16
|
+
self.outputs = []
|
17
|
+
self.output_groups = []
|
15
18
|
end
|
16
19
|
|
17
20
|
def to_s
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module VideoConverter
|
4
|
+
class InputArray
|
5
|
+
attr_accessor :inputs
|
6
|
+
|
7
|
+
def initialize inputs, output_array
|
8
|
+
self.inputs = (inputs.is_a?(Array) ? inputs : [inputs]).map { |input| Input.new(input) }
|
9
|
+
output_array.outputs.each do |output|
|
10
|
+
if [:standard, :segmented].include? output.type
|
11
|
+
self.inputs[self.inputs.index { |input| input.to_s == output.path }.to_i].outputs << output
|
12
|
+
end
|
13
|
+
end
|
14
|
+
self.inputs.each do |input|
|
15
|
+
output_array.playlists.each do |playlist|
|
16
|
+
unless (groups = input.outputs.select { |output| output.playlist == playlist }).empty?
|
17
|
+
input.output_groups << groups
|
18
|
+
end
|
19
|
+
end
|
20
|
+
input.outputs.select { |output| output.playlist.nil? }.each { |output| input.output_groups << [output] }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -63,7 +63,7 @@ module VideoConverter
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def gen_group_playlist playlist
|
66
|
-
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD"
|
66
|
+
res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n"
|
67
67
|
playlist.streams.sort { |s1, s2| s1[:bandwidth].to_i <=> s2[:bandwidth].to_i }.each do |stream|
|
68
68
|
res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{stream[:bandwidth].to_i * 1000}\n"
|
69
69
|
res += File.join('.', stream[:path]) + "\n"
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Output
|
5
5
|
class << self
|
6
|
-
attr_accessor :base_url, :work_dir, :video_bitrate, :audio_bitrate, :segment_seconds, :keyframe_interval, :threads
|
6
|
+
attr_accessor :base_url, :work_dir, :video_bitrate, :audio_bitrate, :segment_seconds, :keyframe_interval, :threads, :video_codec, :audio_codec
|
7
7
|
end
|
8
8
|
|
9
9
|
self.base_url = '/tmp'
|
@@ -13,11 +13,13 @@ module VideoConverter
|
|
13
13
|
self.segment_seconds = 10
|
14
14
|
self.keyframe_interval = 250
|
15
15
|
self.threads = 1
|
16
|
+
self.video_codec = 'libx264'
|
17
|
+
self.audio_codec = 'libfaac'
|
16
18
|
|
17
|
-
attr_accessor :type, :url, :base_url, :filename, :format, :video_bitrate, :uid, :streams, :work_dir, :local_path, :playlist, :items, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :threads
|
19
|
+
attr_accessor :type, :url, :base_url, :filename, :format, :video_bitrate, :uid, :streams, :work_dir, :local_path, :playlist, :items, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :threads, :video_codec, :audio_codec, :path
|
18
20
|
|
19
21
|
def initialize params = {}
|
20
|
-
self.uid = params[:uid]
|
22
|
+
self.uid = params[:uid].to_s
|
21
23
|
|
22
24
|
# General output options
|
23
25
|
self.type = params[:type] ? params[:type].to_sym : :standard
|
@@ -36,7 +38,7 @@ module VideoConverter
|
|
36
38
|
self.base_url = (params[:url] ? File.dirname(params[:url]) : params[:base_url]) || self.class.base_url
|
37
39
|
self.filename = (params[:url] ? File.basename(params[:url]) : params[:filename]) || self.uid + '.' + self.format
|
38
40
|
self.url = params[:url] ? params[:url] : File.join(base_url, filename)
|
39
|
-
self.work_dir = File.join(params[:work_dir] || self.class.work_dir, uid
|
41
|
+
self.work_dir = File.join(params[:work_dir] || self.class.work_dir, uid)
|
40
42
|
format_regexp = Regexp.new("#{File.extname(filename)}$")
|
41
43
|
self.local_path = File.join(work_dir, filename.sub(format_regexp, ".#{format}"))
|
42
44
|
FileUtils.mkdir_p File.dirname(local_path)
|
@@ -45,6 +47,7 @@ module VideoConverter
|
|
45
47
|
FileUtils.mkdir_p chunks_dir
|
46
48
|
end
|
47
49
|
self.threads = self.class.threads
|
50
|
+
self.path = params[:path]
|
48
51
|
|
49
52
|
# Rate controle
|
50
53
|
self.video_bitrate = params[:video_bitrate].to_i > 0 ? params[:video_bitrate].to_i : self.class.video_bitrate
|
@@ -56,10 +59,14 @@ module VideoConverter
|
|
56
59
|
|
57
60
|
# Frame rate
|
58
61
|
self.keyframe_interval = params[:keyframe_interval].to_i > 0 ? params[:keyframe_interval].to_i : self.class.keyframe_interval
|
62
|
+
|
63
|
+
# Format and codecs
|
64
|
+
self.video_codec = (params[:copy_video] ? 'copy' : params[:video_codec]) || self.class.video_codec
|
65
|
+
self.audio_codec = (params[:copy_audio] ? 'copy' : params[:audio_codec]) || self.class.audio_codec
|
59
66
|
end
|
60
67
|
|
61
68
|
def to_hash
|
62
|
-
keys = [:video_bitrate, :local_path, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :threads]
|
69
|
+
keys = [:video_bitrate, :local_path, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :threads, :video_codec, :audio_codec]
|
63
70
|
Hash[*keys.map{ |key| [key, self.send(key)] }.flatten]
|
64
71
|
end
|
65
72
|
|
@@ -17,14 +17,5 @@ module VideoConverter
|
|
17
17
|
def playlists
|
18
18
|
outputs.select { |output| output.type == :playlist }
|
19
19
|
end
|
20
|
-
|
21
|
-
def groups
|
22
|
-
groups = []
|
23
|
-
playlists.each { |playlist| groups << playlist.items }
|
24
|
-
outputs.select { |output| output.playlist.nil? && [:standard, :segmented].include?(output.type) }.each do |output|
|
25
|
-
groups << [output]
|
26
|
-
end
|
27
|
-
groups
|
28
|
-
end
|
29
20
|
end
|
30
21
|
end
|
@@ -3,12 +3,12 @@ require 'test_helper'
|
|
3
3
|
class VideoConverterTest < Test::Unit::TestCase
|
4
4
|
context 'run' do
|
5
5
|
setup do
|
6
|
-
@
|
6
|
+
@input_file = 'test/fixtures/test.mp4'
|
7
|
+
@input_url = 'http://techslides.com/demos/sample-videos/small.mp4'
|
7
8
|
end
|
8
|
-
|
9
9
|
context 'with default type' do
|
10
10
|
setup do
|
11
|
-
@c = VideoConverter.new('input' => @
|
11
|
+
@c = VideoConverter.new('input' => @input_file, 'output' => [{'video_bitrate' => 300, 'filename' => 'tmp/test1.mp4'}, {'video_bitrate' => 700, :filename => 'tmp/test2.mp4'}], 'log' => 'tmp/test.log')
|
12
12
|
@res = @c.run
|
13
13
|
end
|
14
14
|
should 'convert files' do
|
@@ -31,7 +31,7 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
31
31
|
|
32
32
|
context 'with type segmented' do
|
33
33
|
setup do
|
34
|
-
@c = VideoConverter.new(:input => @
|
34
|
+
@c = VideoConverter.new(:input => @input_file, :output => [{:type => :segmented, :video_bitrate => 500, :audio_bitrate => 128, :filename => 'tmp/sd/r500.m3u8'}, {:type => :segmented, :video_bitrate => 700, :audio_bitrate => 128, :filename => 'tmp/sd/r700.m3u8'}, {:type => :segmented, :video_bitrate => 200, :audio_bitrate => 64, :filename => 'tmp/ld/r200.m3u8'}, {:type => :segmented, :video_bitrate => 300, :audio_bitrate => 60, :filename => 'tmp/ld/r300.m3u8'}, {:type => :playlist, :streams => [{'path' => 'tmp/sd/r500.m3u8', 'bandwidth' => 650}, {'path' => 'tmp/sd/r700.m3u8', 'bandwidth' => 850}], :filename => 'tmp/playlist_sd.m3u8'}, {:type => :playlist, :streams => [{'path' => 'tmp/ld/r200.m3u8', 'bandwidth' => 300}, {'path' => 'tmp/ld/r300.m3u8', 'bandwidth' => 400}], :filename => 'tmp/playlist_ld.m3u8'}])
|
35
35
|
@res = @c.run
|
36
36
|
@work_dir = File.join(VideoConverter::Output.work_dir, @c.uid)
|
37
37
|
end
|
@@ -63,5 +63,54 @@ class VideoConverterTest < Test::Unit::TestCase
|
|
63
63
|
assert File.read(playlist).include?('r300.m3u8')
|
64
64
|
end
|
65
65
|
end
|
66
|
+
|
67
|
+
context 'only segment some files' do
|
68
|
+
setup do
|
69
|
+
# prepare files
|
70
|
+
@c1 = VideoConverter.new(:input => @input_file, :output => [{:video_bitrate => 300, :filename => 'sd/1.mp4'}, {:video_bitrate => 400, :filename => 'sd/2.mp4'}, {:video_bitrate => 500, :filename => 'hd/1.mp4'}, {:video_bitrate => 600, :filename => 'hd/2.mp4'}])
|
71
|
+
@c1.run
|
72
|
+
@input1, @input2, @input3, @input4 = @c1.output_array.outputs.map { |output| output.local_path }
|
73
|
+
# test segmentation
|
74
|
+
@c2 = VideoConverter.new(:input => [@input1, @input2, @input3, @input4], :output => [
|
75
|
+
{:path => @input1, :type => :segmented, :filename => '1.m3u8', :copy_video => true},
|
76
|
+
{:path => @input2, :type => :segmented, :filename => '2.m3u8', :copy_video => true},
|
77
|
+
{:path => @input3, :type => :segmented, :filename => '3.m3u8', :copy_video => true},
|
78
|
+
{:path => @input4, :type => :segmented, :filename => '4.m3u8', :copy_video => true},
|
79
|
+
{:type => :playlist, :streams => [{:path => '1.m3u8'}, {:path => '2.m3u8'}], :filename => 'playlist1.m3u8'},
|
80
|
+
{:type => :playlist, :streams => [{:path => '3.m3u8'}, {:path => '4.m3u8'}], :filename => 'playlist2.m3u8'}
|
81
|
+
])
|
82
|
+
@res = @c2.run
|
83
|
+
@work_dir = File.join(VideoConverter::Output.work_dir, @c2.uid)
|
84
|
+
end
|
85
|
+
should 'create chunks' do
|
86
|
+
4.times do |n|
|
87
|
+
assert Dir.entries(File.join(@work_dir, "#{n + 1}")).count > 0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
should 'create quality playlists' do
|
91
|
+
4.times do |n|
|
92
|
+
playlist = "#{n + 1}.m3u8"
|
93
|
+
assert File.exists?(File.join(@work_dir, playlist))
|
94
|
+
assert File.read(File.join(@work_dir, playlist)).include?('s-00001')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
should 'create group playlist' do
|
98
|
+
playlist = File.join(@work_dir, 'playlist1.m3u8')
|
99
|
+
assert File.exists?(playlist)
|
100
|
+
assert File.read(playlist).include?('1.m3u8')
|
101
|
+
assert File.read(playlist).include?('2.m3u8')
|
102
|
+
|
103
|
+
playlist = File.join(@work_dir, 'playlist2.m3u8')
|
104
|
+
assert File.exists?(playlist)
|
105
|
+
assert File.read(playlist).include?('3.m3u8')
|
106
|
+
assert File.read(playlist).include?('4.m3u8')
|
107
|
+
end
|
108
|
+
should 'not convert inputs' do
|
109
|
+
assert_equal VideoConverter::Input.new(@input1).metadata[:video_bitrate_in_kbps], VideoConverter::Input.new(File.join(@work_dir, '1.ts')).metadata[:video_bitrate_in_kbps]
|
110
|
+
assert_equal VideoConverter::Input.new(@input2).metadata[:video_bitrate_in_kbps], VideoConverter::Input.new(File.join(@work_dir, '2.ts')).metadata[:video_bitrate_in_kbps]
|
111
|
+
assert_equal VideoConverter::Input.new(@input3).metadata[:video_bitrate_in_kbps], VideoConverter::Input.new(File.join(@work_dir, '3.ts')).metadata[:video_bitrate_in_kbps]
|
112
|
+
assert_equal VideoConverter::Input.new(@input4).metadata[:video_bitrate_in_kbps], VideoConverter::Input.new(File.join(@work_dir, '4.ts')).metadata[:video_bitrate_in_kbps]
|
113
|
+
end
|
114
|
+
end
|
66
115
|
end
|
67
116
|
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.2.
|
4
|
+
version: 0.2.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: 2013-04-
|
12
|
+
date: 2013-04-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -94,6 +94,7 @@ files:
|
|
94
94
|
- lib/video_converter/command.rb
|
95
95
|
- lib/video_converter/ffmpeg.rb
|
96
96
|
- lib/video_converter/input.rb
|
97
|
+
- lib/video_converter/input_array.rb
|
97
98
|
- lib/video_converter/live_segmenter.rb
|
98
99
|
- lib/video_converter/output.rb
|
99
100
|
- lib/video_converter/output_array.rb
|
@@ -119,7 +120,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
120
|
version: '0'
|
120
121
|
segments:
|
121
122
|
- 0
|
122
|
-
hash:
|
123
|
+
hash: 3078997600954711389
|
123
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
125
|
none: false
|
125
126
|
requirements:
|
@@ -128,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
129
|
version: '0'
|
129
130
|
segments:
|
130
131
|
- 0
|
131
|
-
hash:
|
132
|
+
hash: 3078997600954711389
|
132
133
|
requirements:
|
133
134
|
- ffmpeg, version 1.2 or greated configured with libx264 and libfaac
|
134
135
|
- live_segmenter to convert to hls
|