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.
@@ -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
@@ -2,16 +2,13 @@
2
2
 
3
3
  module VideoConverter
4
4
  class Base
5
- attr_accessor :input, :output_array, :log, :uid
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
- raise ArgumentError.new('output is needed') if params[:output].nil? || params[:output].empty?
14
- self.output_array = OutputArray.new(params[:output], uid)
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
- [:input, :output_array, :log].each do |param|
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 libx264 -g 100 -keyint_min 50 -b:v %{video_bitrate}k -bt %{video_bitrate}k -f mp4 %{local_path} 1>%{log} 2>&1 || exit 1"
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 libx264 -g %{keyframe_interval} -keyint_min 25 -pass 1 -passlogfile %{input}.log -b:v 700k -threads %{threads} -f mp4 /dev/null 1>>%{log} 2>&1 || exit 1"
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 %{input}.log -c:a libfaac -b:a %{audio_bitrate}k -c:v libx264 -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"
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 :input, :output_array, :one_pass, :paral, :log
20
+ attr_accessor :input_array, :output_array, :one_pass, :paral, :log
21
21
 
22
22
  def initialize params
23
- [:input, :output_array].each do |param|
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
- threads = []
34
- output_array.groups.each do |group|
35
- unless one_pass
36
- 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 {})))
37
- res &&= first_pass_command.execute
38
- end
39
- group.each do |quality|
40
- if one_pass
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
- if paral
46
- threads << Thread.new { res &&= quality_command.execute }
47
- else
48
- res &&= quality_command.execute
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, :input => input, :log => log }
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.to_s)
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
@@ -1,3 +1,3 @@
1
1
  module VideoConverter
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  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
- @input = 'test/fixtures/test.mp4'
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' => @input, 'output' => [{'video_bitrate' => 300, 'filename' => 'tmp/test1.mp4'}, {'video_bitrate' => 700, :filename => 'tmp/test2.mp4'}], 'log' => 'tmp/test.log')
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 => @input, :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'}])
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.1
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-11 00:00:00.000000000 Z
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: 690094087006965726
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: 690094087006965726
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