video_converter 0.2.1 → 0.2.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.
- 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
|