video_converter 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,28 +1,27 @@
1
1
  require "video_converter/version"
2
- require "video_converter/profile"
3
2
  require "video_converter/base"
4
3
  require "video_converter/command"
5
4
  require "video_converter/process"
6
5
  require "video_converter/ffmpeg"
7
6
  require "video_converter/live_segmenter"
8
7
  require "video_converter/input"
8
+ require "video_converter/output"
9
+ require "video_converter/output_array"
9
10
  require "fileutils"
10
11
  require "net/http"
11
12
 
12
13
  module VideoConverter
13
14
  class << self
14
- attr_accessor :type, :paral, :no_convert
15
+ attr_accessor :paral
15
16
  end
16
17
 
17
- self.type = :mp4
18
18
  self.paral = true
19
- self.no_convert = false
20
19
 
21
20
  def self.new params
22
21
  VideoConverter::Base.new params
23
22
  end
24
23
 
25
- def self.find id
26
- VideoConverter::Process.new id
24
+ def self.find uid
25
+ VideoConverter::Process.new uid
27
26
  end
28
27
  end
@@ -2,38 +2,29 @@
2
2
 
3
3
  module VideoConverter
4
4
  class Base
5
- attr_accessor :input, :profile, :one_pass, :type, :paral, :log, :id, :playlist_dir, :chunk_base, :no_convert
5
+ attr_accessor :input, :output_array, :log, :uid
6
6
 
7
7
  def initialize params
8
- self.no_convert = params[:no_convert].nil? ? VideoConverter.no_convert : params[:no_convert]
9
- unless no_convert
10
- self.input = params[:input] or raise ArgumentError.new('Input is needed')
11
- raise ArgumentError.new('Input does not exist') unless File.exists?(input)
12
- end
13
- self.profile = params[:profile] or raise ArgumentError.new('Profile is needed')
14
- self.type = params[:type].nil? ? VideoConverter.type : params[:type].to_sym
15
- raise ArgumentError.new("Incorrect type #{type}") unless [:hls, :mp4].include?(type)
16
- if type == :hls
17
- self.playlist_dir = params[:playlist_dir] or raise ArgumentError.new("Playlist dir is needed")
18
- end
19
- self.one_pass = params[:one_pass].nil? ? Ffmpeg.one_pass : params[:one_pass]
20
- self.paral = params[:paral].nil? ? VideoConverter.paral : params[:paral]
8
+ raise ArgumentError.new('input is needed') if params[:input].nil? || params[:input].empty?
9
+ self.input = Input.new(params[:input])
10
+ raise ArgumentError.new('input does not exist') unless input.exists?
11
+ self.uid = params[:uid] || (Socket.gethostname + object_id.to_s)
12
+ raise ArgumentError.new('output is needed') if params[:output].nil? || params[:output].empty?
13
+ self.output_array = OutputArray.new(params[:output], uid)
21
14
  if params[:log].nil?
22
15
  self.log = '/dev/null'
23
16
  else
24
17
  self.log = params[:log]
25
18
  FileUtils.mkdir_p File.dirname(log)
26
19
  end
27
- self.id = object_id
28
- self.chunk_base = params[:chunk_base]
29
20
  end
30
21
 
31
22
  def run
32
- process = VideoConverter::Process.new(id)
23
+ process = VideoConverter::Process.new(uid)
24
+ process.status = 'started'
33
25
  process.pid = `cat /proc/self/stat`.split[3]
34
26
  actions = []
35
- actions << :convert unless no_convert
36
- actions << :live_segment if type == :hls
27
+ actions = [:convert, :segment]
37
28
  actions.each do |action|
38
29
  process.status = action.to_s
39
30
  process.progress = 0
@@ -45,6 +36,7 @@ module VideoConverter
45
36
  return false
46
37
  end
47
38
  end
39
+ process.status = 'finished'
48
40
  true
49
41
  end
50
42
 
@@ -52,18 +44,17 @@ module VideoConverter
52
44
 
53
45
  def convert
54
46
  params = {}
55
- [:input, :profile, :one_pass, :paral, :log].each do |param|
47
+ [:input, :output_array, :log].each do |param|
56
48
  params[param] = self.send(param)
57
49
  end
58
50
  Ffmpeg.new(params).run
59
51
  end
60
52
 
61
- def live_segment
53
+ def segment
62
54
  params = {}
63
- [:profile, :playlist_dir, :paral, :chunk_base, :log, :no_convert].each do |param|
55
+ [:output_array, :log].each do |param|
64
56
  params[param] = self.send(param)
65
57
  end
66
- params[:delete_input] = false if no_convert
67
58
  LiveSegmenter.new(params).run
68
59
  end
69
60
  end
@@ -3,40 +3,44 @@
3
3
  module VideoConverter
4
4
  class Ffmpeg
5
5
  class << self
6
- attr_accessor :bin, :one_pass, :one_pass_command, :first_pass_command, :second_pass_command
6
+ attr_accessor :bin, :one_pass, :paral, :log, :one_pass_command, :first_pass_command, :second_pass_command
7
7
  end
8
8
 
9
9
  self.bin = '/usr/local/bin/ffmpeg'
10
-
11
10
  self.one_pass = false
11
+ self.paral = true
12
+ self.log = '/dev/null'
12
13
 
13
- self.one_pass_command = "%{bin} -i %{input} -y -aspect %{aspect} -acodec copy -vcodec libx264 -g 100 -keyint_min 50 -b:v %{bitrate}k -bt %{bitrate}k -threads %{threads} -f mp4 %{file} 1>%{log} 2>&1 || exit 1"
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
15
 
15
- self.first_pass_command = "%{bin} -i %{input} -y -aspect %{aspect} -an -vcodec libx264 -g 100 -keyint_min 50 -pass 1 -passlogfile %{input}.log -b:v 700k -bt 700k -threads %{threads} -f mp4 /dev/null 1>>%{log} 2>&1 || exit 1"
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
17
 
17
- self.second_pass_command = "%{bin} -i %{input} -y -aspect %{aspect} -acodec copy -vcodec libx264 -g 100 -keyint_min 50 -pass 2 -passlogfile %{input}.log -b:v %{bitrate}k -bt %{bitrate}k -threads %{threads} -f mp4 %{file} 1>%{log} 2>&1 || exit 1"
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
19
 
19
- attr_accessor :input, :profile, :one_pass, :paral, :log
20
+ attr_accessor :input, :output_array, :one_pass, :paral, :log
20
21
 
21
22
  def initialize params
22
- params.each do |param, value|
23
- self.send("#{param}=", value)
23
+ [:input, :output_array].each do |param|
24
+ self.send("#{param}=", params[param]) or raise ArgumentError.new("#{param} is needed")
25
+ end
26
+ [:one_pass, :paral, :log].each do |param|
27
+ self.send("#{param}=", params[param] ? params[param] : self.class.send(param))
24
28
  end
25
29
  end
26
30
 
27
31
  def run
28
32
  res = true
29
33
  threads = []
30
- Profile.groups(profile).each do |qualities|
34
+ output_array.groups.each do |group|
31
35
  unless one_pass
32
- group_command = Command.new self.class.first_pass_command, common_params.merge(qualities.first.to_hash)
33
- res &&= group_command.execute
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
34
38
  end
35
- qualities.each do |quality|
39
+ group.each do |quality|
36
40
  if one_pass
37
- quality_command = Command.new self.class.one_pass_command, common_params.merge(quality.to_hash)
41
+ quality_command = Command.new self.class.one_pass_command, prepare_params(common_params.merge(quality.to_hash))
38
42
  else
39
- quality_command = Command.new self.class.second_pass_command, common_params.merge(quality.to_hash)
43
+ quality_command = Command.new self.class.second_pass_command, prepare_params(common_params.merge(quality.to_hash))
40
44
  end
41
45
  if paral
42
46
  threads << Thread.new { res &&= quality_command.execute }
@@ -54,5 +58,11 @@ module VideoConverter
54
58
  def common_params
55
59
  { :bin => self.class.bin, :input => input, :log => log }
56
60
  end
61
+
62
+ def prepare_params params
63
+ params[:size] = params[:size] ? "-s #{params[:size]}" : ''
64
+ params[:frame_rate] = params[:frame_rate] ? "-r #{params[:frame_rate]}" : ''
65
+ params
66
+ end
57
67
  end
58
68
  end
@@ -14,6 +14,10 @@ module VideoConverter
14
14
  self.input = input
15
15
  end
16
16
 
17
+ def to_s
18
+ input
19
+ end
20
+
17
21
  def exists?
18
22
  if is_http?
19
23
  url = URI.parse(input)
@@ -65,8 +69,6 @@ module VideoConverter
65
69
  metadata
66
70
  end
67
71
 
68
- private
69
-
70
72
  def is_http?
71
73
  !!input.match(/^http:\/\//)
72
74
  end
@@ -75,6 +77,8 @@ module VideoConverter
75
77
  File.file?(input)
76
78
  end
77
79
 
80
+ private
81
+
78
82
  def common_params
79
83
  { :bin => VideoConverter::Ffmpeg.bin, :input => input }
80
84
  end
@@ -3,92 +3,78 @@
3
3
  module VideoConverter
4
4
  class LiveSegmenter
5
5
  class << self
6
- attr_accessor :bin, :ffprobe_bin, :chunks_command, :segment_length, :filename_prefix, :encoding_profile, :delete_input
6
+ attr_accessor :bin, :ffprobe_bin, :chunks_command, :chunk_prefix, :encoding_profile, :log, :paral
7
7
  end
8
8
 
9
9
  self.bin = '/usr/local/bin/live_segmenter'
10
-
11
10
  self.ffprobe_bin = '/usr/local/bin/ffprobe'
12
-
13
- self.segment_length = 10
14
-
15
- self.filename_prefix = 's'
16
-
11
+ self.chunk_prefix = 's'
17
12
  self.encoding_profile = 's'
13
+ self.log = '/dev/null'
14
+ self.paral = true
18
15
 
19
- self.delete_input = true
20
-
21
- self.chunks_command = '%{ffmpeg_bin} -i %{input} -vcodec libx264 -acodec copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{segment_length} %{dir} %{filename_prefix} %{encoding_profile} 1>>%{log} 2>&1'
16
+ self.chunks_command = '%{ffmpeg_bin} -i %{local_path} -vcodec libx264 -acodec copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{segment_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
22
17
 
23
- attr_accessor :profile, :playlist_dir, :paral, :segment_length, :filename_prefix, :encoding_profile, :delete_input, :chunk_base, :log
18
+ attr_accessor :paral, :chunk_prefix, :encoding_profile, :log, :output_array
24
19
 
25
20
  def initialize params
26
- [:profile, :playlist_dir].each do |param|
27
- self.send("#{param}=", params[param])
28
- end
29
- [:segment_length, :filename_prefix, :encoding_profile, :delete_input].each do |param|
21
+ self.output_array = params[:output_array] or raise ArgumentError.new("output_array is needed")
22
+ [:chunk_prefix, :encoding_profile, :log, :paral].each do |param|
30
23
  self.send("#{param}=", params[param].nil? ? self.class.send(param) : params[param])
31
24
  end
32
- self.chunk_base = params[:chunk_base] ? params[:chunk_base] : '.'
33
- self.chunk_base += '/' unless chunk_base.end_with?('/')
34
- self.log = params[:log]
35
25
  end
36
26
 
37
27
  def run
38
28
  res = true
39
29
  threads = []
40
- p = Proc.new do |profile|
41
- input = profile.to_hash[:file]
42
- output = profile.to_hash[:dir]
43
- make_chunks(input, output) && gen_quality_playlist(output, "#{File.basename(output)}.m3u8")
30
+ p = Proc.new do |output|
31
+ make_chunks(output) && gen_quality_playlist(output)
44
32
  end
45
- [profile].flatten.each do |profile|
46
- if paral
47
- threads << Thread.new { res &&= p.call(profile) }
48
- else
49
- res &&= p.call(profile)
33
+ output_array.playlists.each do |playlist|
34
+ playlist.items.each do |item|
35
+ if paral
36
+ threads << Thread.new { res &&= p.call(item) }
37
+ else
38
+ res &&= p.call(item)
39
+ end
50
40
  end
41
+ res &&= gen_group_playlist playlist
51
42
  end
52
- gen_group_playlists
43
+ threads.each { |t| t.join } if paral
44
+ res
53
45
  end
54
46
 
55
47
  private
56
48
 
57
- def make_chunks input, output
58
- params = {}
59
- [:segment_length, :filename_prefix, :encoding_profile].each { |param| params[param] = self.send(param) }
60
- command = Command.new self.class.chunks_command, params.merge(:input => input, :dir => output).merge(common_params)
61
- res = command.execute
62
- FileUtils.rm input if delete_input
63
- res
49
+ def make_chunks output
50
+ Command.new(self.class.chunks_command, common_params.merge(output.to_hash)).execute
64
51
  end
65
52
 
66
- def gen_quality_playlist chunks_dir, playlist_name
53
+ def gen_quality_playlist output
67
54
  res = ''
68
55
  durations = []
69
- Dir::glob(File.join(chunks_dir, 's-*[0-9].ts')).each do |chunk|
56
+ Dir::glob(File.join(output.chunks_dir, "#{chunk_prefix}-*[0-9].ts")).sort { |c1, c2| File.basename(c1).match(/\d+/).to_s.to_i <=> File.basename(c2).match(/\d+/).to_s.to_i }.each do |chunk|
70
57
  durations << (duration = chunk_duration chunk)
71
58
  res += "#EXTINF:#%0.2f\n" % duration
72
- res += "#{chunk_base}#{File.basename(chunks_dir)}/#{File.basename(chunk)}\n"
59
+ res += './' + File.join(File.basename(output.chunks_dir), File.basename(chunk)) + "\n"
73
60
  end
74
61
  res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
75
- File.open(File.join(playlist_dir, playlist_name), 'w') { |f| f.write res }
62
+ File.open(File.join(output.work_dir, output.filename), 'w') { |f| f.write res }
76
63
  end
77
64
 
78
- def gen_group_playlists
79
- Profile.groups(profile).each_with_index do |group, index|
80
- res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD"
81
- group.sort { |g1, g2| g1.to_hash[:bandwidth] <=> g2.to_hash[:bandwidth] }.each do |quality|
82
- res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{quality.to_hash[:bandwidth] * 1000}\n"
83
- res += File.join(chunk_base, playlist_dir, File.basename(quality.to_hash[:dir]) + '.m3u8')
84
- end
85
- res += "#EXT-X-ENDLIST"
86
- File.open(File.join(playlist_dir, "playlist#{index + 1}.m3u8"), 'w') { |f| f.write res }
65
+ def gen_group_playlist playlist
66
+ res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD"
67
+ playlist.streams.sort { |s1, s2| s1['bandwidth'].to_i <=> s2['bandwidth'].to_i }.each do |stream|
68
+ res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{stream['bandwidth'].to_i * 1000}\n"
69
+ res += File.join('.', stream['path']) + "\n"
87
70
  end
71
+ res += "#EXT-X-ENDLIST"
72
+ File.open(File.join(playlist.work_dir, playlist.filename), 'w') { |f| f.write res }
73
+ true
88
74
  end
89
75
 
90
76
  def common_params
91
- { :ffmpeg_bin => Ffmpeg.bin, :bin => self.class.bin, :log => log }
77
+ { :ffmpeg_bin => Ffmpeg.bin, :bin => self.class.bin, :log => log, :chunk_prefix => chunk_prefix, :encoding_profile => encoding_profile }
92
78
  end
93
79
 
94
80
  def chunk_duration chunk
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+
3
+ module VideoConverter
4
+ class Output
5
+ class << self
6
+ attr_accessor :base_url, :work_dir, :video_bitrate, :audio_bitrate, :segment_seconds, :keyframe_interval, :threads
7
+ end
8
+
9
+ self.base_url = '/tmp'
10
+ self.work_dir = '/tmp'
11
+ self.video_bitrate = 700
12
+ self.audio_bitrate = 200
13
+ self.segment_seconds = 10
14
+ self.keyframe_interval = 250
15
+ self.threads = 1
16
+
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
18
+
19
+ def initialize params = {}
20
+ self.uid = params[:uid]
21
+
22
+ # General output options
23
+ self.type = params[:type] ? params[:type].to_sym : :standard
24
+ raise ArgumentError.new('Incorrect type') unless %w(standard segmented playlist transfer-only).include?(type.to_s)
25
+
26
+ self.format = params[:format]
27
+ if !format && params[:filename]
28
+ self.format = File.extname(params[:filename]).sub('.', '')
29
+ end
30
+ if format == 'm3u8' || !format && type == :segmented
31
+ self.format = 'ts'
32
+ end
33
+ self.format = 'mp4' if format.nil? || format.empty?
34
+ raise ArgumentError.new('Incorrect format') unless %w(3g2 3gp 3gp2 3gpp 3gpp2 aac ac3 eac3 ec3 f4a f4b f4v flv highwinds m4a m4b m4r m4v mkv mov mp3 mp4 oga ogg ogv ogx ts webm wma wmv).include?(format)
35
+
36
+ self.base_url = (params[:url] ? File.dirname(params[:url]) : params[:base_url]) || self.class.base_url
37
+ self.filename = (params[:url] ? File.basename(params[:url]) : params[:filename]) || self.uid + '.' + self.format
38
+ 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)
40
+ format_regexp = Regexp.new("#{File.extname(filename)}$")
41
+ self.local_path = File.join(work_dir, filename.sub(format_regexp, ".#{format}"))
42
+ FileUtils.mkdir_p File.dirname(local_path)
43
+ if type == :segmented
44
+ self.chunks_dir = File.join(work_dir, filename.sub(format_regexp, ''))
45
+ FileUtils.mkdir_p chunks_dir
46
+ end
47
+ self.threads = self.class.threads
48
+
49
+ # Rate controle
50
+ self.video_bitrate = params[:video_bitrate].to_i > 0 ? params[:video_bitrate].to_i : self.class.video_bitrate
51
+ self.audio_bitrate = params[:audio_bitrate].to_i > 0 ? params[:audio_bitrate].to_i : self.class.audio_bitrate
52
+
53
+ # Segmented streaming
54
+ self.streams = params[:streams].to_a
55
+ self.segment_seconds = params[:segment_seconds] || self.class.segment_seconds
56
+
57
+ # Frame rate
58
+ self.keyframe_interval = params[:keyframe_interval].to_i > 0 ? params[:keyframe_interval].to_i : self.class.keyframe_interval
59
+ end
60
+
61
+ def to_hash
62
+ keys = [:video_bitrate, :local_path, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :threads]
63
+ Hash[*keys.map{ |key| [key, self.send(key)] }.flatten]
64
+ end
65
+
66
+ def self.outputs output
67
+ (output.is_a?(Hash) ? [output] : output).map { |output| output.is_a?(self.class) ? output : new(output) }
68
+ end
69
+
70
+ def self.playlists output
71
+ outputs(output).select { |output| output.type == :playlist }
72
+ end
73
+
74
+ def qualities output
75
+ raise TypeError.new('Only for playlists') unless type == :playlist
76
+ stream_paths = streams.map { |stream| stream['path'] }
77
+ self.class.outputs(output).select { |output| stream_paths.include? output.filename }
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module VideoConverter
4
+ class OutputArray
5
+ attr_accessor :outputs, :uid
6
+
7
+ def initialize outputs, uid
8
+ self.uid = uid
9
+ self.outputs = (outputs.is_a?(Array) ? outputs : [outputs]).map { |output| Output.new(output.merge(:uid => uid)) }
10
+ self.outputs.select { |output| output.type == :playlist }.each do |playlist|
11
+ stream_names = playlist.streams.map { |stream| stream['path'] }
12
+ playlist.items = self.outputs.select { |output| [:standard, :segmented].include?(output.type) && stream_names.include?(output.filename) }
13
+ playlist.items.each { |item| item.playlist = playlist }
14
+ end
15
+ end
16
+
17
+ def playlists
18
+ outputs.select { |output| output.type == :playlist }
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
+ end
30
+ end
@@ -2,15 +2,15 @@
2
2
 
3
3
  module VideoConverter
4
4
  class Process
5
- attr_accessor :id, :status, :progress, :pid
5
+ attr_accessor :uid, :status, :progress, :pid
6
6
 
7
7
  class << self
8
8
  attr_accessor :path
9
9
  end
10
10
  self.path = 'tmp/processes'
11
11
 
12
- def initialize id
13
- self.id = id
12
+ def initialize uid
13
+ self.uid = uid
14
14
  Dir.mkdir(self.class.path) unless Dir.exists?(self.class.path)
15
15
  end
16
16
 
@@ -26,7 +26,7 @@ module VideoConverter
26
26
  private
27
27
 
28
28
  [:status, :progress, :pid].each do |attr|
29
- define_method("#{attr}_file".to_sym) { File.join self.class.path, "#{id}_#{attr}" }
29
+ define_method("#{attr}_file".to_sym) { File.join self.class.path, "#{uid}_#{attr}" }
30
30
  end
31
31
  end
32
32
  end
@@ -1,3 +1,3 @@
1
1
  module VideoConverter
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -6,27 +6,22 @@ class VideoConverterTest < Test::Unit::TestCase
6
6
  @input = 'test/fixtures/test.mp4'
7
7
  end
8
8
 
9
- context 'with type mp4' do
9
+ context 'with default type' do
10
10
  setup do
11
- @profiles = []
12
- @profiles << (@p11 = VideoConverter::Profile.new(:bitrate => 300, :file => 'tmp/test11.mp4'))
13
- @profiles << (@p12 = VideoConverter::Profile.new(:bitrate => 400, :file => 'tmp/test12.mp4'))
14
- @profiles << (@p21 = VideoConverter::Profile.new(:bitrate => 700, :file => 'tmp/test21.mp4'))
15
- @profiles << (@p22 = VideoConverter::Profile.new(:bitrate => 700, :file => 'tmp/test22.mp4'))
16
- @c = VideoConverter.new(:input => @input, :profile => [[@p11, @p12], [@p21, @p22]], :verbose => false, :log => 'tmp/test.log', :type => :mp4)
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')
17
12
  @res = @c.run
18
13
  end
19
14
  should 'convert files' do
20
- 4.times do |n|
21
- file = "tmp/test#{n / 2 + 1}#{n.even? ? 1 : 2}.mp4"
15
+ 2.times do |n|
16
+ file = File.join(VideoConverter::Output.work_dir, @c.uid, "tmp/test#{n + 1}.mp4")
22
17
  assert File.exists?(file)
23
18
  assert File.size(file) > 0
24
19
  end
25
20
  end
26
21
  should 'return success convert process' do
27
- assert VideoConverter.find(@c.id)
22
+ assert VideoConverter.find(@c.uid)
28
23
  assert @res
29
- assert_equal 'convert_success', VideoConverter.find(@c.id).status
24
+ assert_equal 'finished', VideoConverter.find(@c.uid).status
30
25
  end
31
26
  should 'write log file' do
32
27
  assert File.exists?('tmp/test.log')
@@ -34,57 +29,40 @@ class VideoConverterTest < Test::Unit::TestCase
34
29
  end
35
30
  end
36
31
 
37
- context 'with type hls' do
32
+ context 'with type segmented' do
38
33
  setup do
39
- @profiles = []
40
- @profiles << (@p11 = VideoConverter::Profile.new(:bitrate => 300, :dir => 'tmp/test11'))
41
- @profiles << (@p12 = VideoConverter::Profile.new(:bitrate => 400, :dir => 'tmp/test12'))
42
- @profiles << (@p21 = VideoConverter::Profile.new(:bitrate => 700, :dir => 'tmp/test21'))
43
- @profiles << (@p22 = VideoConverter::Profile.new(:bitrate => 700, :dir => 'tmp/test22'))
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'}])
35
+ @res = @c.run
36
+ @work_dir = File.join(VideoConverter::Output.work_dir, @c.uid)
37
+ puts @work_dir
44
38
  end
45
- context '' do
46
- setup do
47
- @c = VideoConverter.new(:input => @input, :profile => [[@p11, @p12], [@p21, @p22]], :verbose => false, :log => 'tmp/test.log', :type => :hls, :playlist_dir => 'tmp')
48
- @res = @c.run
49
- end
50
- should 'create chunks' do
51
- @profiles.each do |profile|
52
- assert File.exists?(File.join(profile.to_hash[:dir], 's-00001.ts'))
53
- end
54
- end
55
- should 'create quality playlists' do
56
- @profiles.each do |profile|
57
- assert File.exists?(File.join(File.dirname(profile.to_hash[:dir]), File.basename(profile.to_hash[:dir]) + '.m3u8'))
58
- end
59
- end
60
- should 'create group playlists' do
61
- playlist1 = File.join('tmp', 'playlist1.m3u8')
62
- playlist2 = File.join('tmp', 'playlist2.m3u8')
63
- assert File.exists? playlist1
64
- assert File.exists? playlist2
65
- assert File.read(playlist1).include?('test11')
66
- assert File.read(playlist1).include?('test12')
67
- assert !File.read(playlist1).include?('test21')
68
- assert !File.read(playlist1).include?('test22')
69
- assert File.read(playlist2).include?('test21')
70
- assert File.read(playlist2).include?('test22')
71
- assert !File.read(playlist2).include?('test11')
72
- assert !File.read(playlist2).include?('test12')
73
- end
39
+ should 'create chunks' do
40
+ assert Dir.entries(File.join(@work_dir, 'tmp/sd/r500')).count > 0
41
+ assert Dir.entries(File.join(@work_dir, 'tmp/sd/r700')).count > 0
42
+ assert Dir.entries(File.join(@work_dir, 'tmp/ld/r200')).count > 0
43
+ assert Dir.entries(File.join(@work_dir, 'tmp/ld/r300')).count > 0
74
44
  end
75
-
76
- context 'with no_convert flag' do
77
- setup do
78
- @c = VideoConverter.new(:profile => VideoConverter::Profile.new(:file => 'test/fixtures/test.mp4', :dir => '/tmp/test', :bitrate => 300), :no_convert => true, :type => :hls, :playlist_dir => 'tmp')
79
- @res = @c.run
80
- end
81
- should 'return true' do
82
- assert @res
83
- end
84
- should 'create needed files' do
85
- assert File.exists? '/tmp/test/s-00001.ts'
45
+ should 'create quality playlists' do
46
+ %w(tmp/sd/r500.m3u8 tmp/sd/r700.m3u8 tmp/ld/r200.m3u8 tmp/ld/r300.m3u8).each do |playlist|
47
+ assert File.exists?(File.join(@work_dir, playlist))
48
+ assert File.read(File.join(@work_dir, playlist)).include?('s-00001')
86
49
  end
87
50
  end
51
+ should 'create group playlists' do
52
+ playlist = File.join(@work_dir, 'tmp/playlist_sd.m3u8')
53
+ assert File.exists?(playlist)
54
+ assert File.read(playlist).include?('r500.m3u8')
55
+ assert File.read(playlist).include?('r700.m3u8')
56
+ assert !File.read(playlist).include?('r200.m3u8')
57
+ assert !File.read(playlist).include?('r300.m3u8')
58
+
59
+ playlist = File.join(@work_dir, 'tmp/playlist_ld.m3u8')
60
+ assert File.exists?(playlist)
61
+ assert !File.read(playlist).include?('r500.m3u8')
62
+ assert !File.read(playlist).include?('r700.m3u8')
63
+ assert File.read(playlist).include?('r200.m3u8')
64
+ assert File.read(playlist).include?('r300.m3u8')
65
+ end
88
66
  end
89
67
  end
90
68
  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.1.1
4
+ version: 0.2.0
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-02 00:00:00.000000000 Z
12
+ date: 2013-04-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -95,8 +95,9 @@ files:
95
95
  - lib/video_converter/ffmpeg.rb
96
96
  - lib/video_converter/input.rb
97
97
  - lib/video_converter/live_segmenter.rb
98
+ - lib/video_converter/output.rb
99
+ - lib/video_converter/output_array.rb
98
100
  - lib/video_converter/process.rb
99
- - lib/video_converter/profile.rb
100
101
  - lib/video_converter/version.rb
101
102
  - test/fixtures/test.mp4
102
103
  - test/fixtures/test.mp4.log-0.log
@@ -120,7 +121,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
121
  version: '0'
121
122
  segments:
122
123
  - 0
123
- hash: -530549277502155145
124
+ hash: 476545178248555902
124
125
  required_rubygems_version: !ruby/object:Gem::Requirement
125
126
  none: false
126
127
  requirements:
@@ -129,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
130
  version: '0'
130
131
  segments:
131
132
  - 0
132
- hash: -530549277502155145
133
+ hash: 476545178248555902
133
134
  requirements:
134
135
  - ffmpeg, version 1.2 or greated configured with libx264 and libfaac
135
136
  - live_segmenter to convert to hls
@@ -1,37 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module VideoConverter
4
- class Profile
5
- class << self
6
- attr_accessor :needed_params, :default_params
7
- end
8
-
9
- self.needed_params = [:bitrate]
10
- self.default_params = {:aspect => '4:3', :threads => 1}
11
-
12
- attr_accessor :params
13
-
14
- def initialize params
15
- self.class.needed_params.each do |needed_param|
16
- raise ArgumentError.new("#{needed_param} is needed") unless params[needed_param]
17
- end
18
- self.params = self.class.default_params.merge params
19
- raise ArgumentError.new("Output file or output dir is needed") unless params[:file] || params[:dir]
20
- self.params[:dir] = params[:dir] || File.dirname(params[:file])
21
- self.params[:file] = params[:file] || File.join(params[:dir], "#{object_id}.mp4")
22
- FileUtils.mkdir_p self.params[:dir]
23
- self.params[:bandwidth] = params[:bandwidth] || params[:bitrate]
24
- end
25
-
26
- def to_hash
27
- params
28
- end
29
-
30
- def self.groups profiles
31
- groups = profiles.is_a?(Array) ? profiles : [profiles]
32
- groups.map do |qualities|
33
- qualities.is_a?(Array) ? qualities : [qualities]
34
- end
35
- end
36
- end
37
- end