video_converter 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,45 +1,29 @@
1
- require "video_converter/version"
1
+ require "fileutils"
2
+ require "net/http"
3
+ require "shellwords"
4
+ require "video_converter/array"
2
5
  require "video_converter/base"
3
6
  require "video_converter/command"
4
- require "video_converter/process"
5
7
  require "video_converter/ffmpeg"
6
- require "video_converter/live_segmenter"
8
+ require "video_converter/hash"
9
+ require "video_converter/hls"
7
10
  require "video_converter/input"
8
- require "video_converter/input_array"
11
+ require "video_converter/live_segmenter"
12
+ require "video_converter/object"
9
13
  require "video_converter/output"
10
- require "video_converter/output_array"
11
- require "video_converter/hls"
12
- require "fileutils"
13
- require "net/http"
14
+ require "video_converter/version"
14
15
  require "video_screenshoter"
15
- require "shellwords"
16
16
 
17
17
  module VideoConverter
18
18
  class << self
19
- attr_accessor :paral
19
+ attr_accessor :log, :paral
20
20
  end
21
+ # TODO log into tmp log file, merge to main log, delete tmp log
22
+ self.log = 'log/convertation.log'
23
+ # TODO output option
21
24
  self.paral = true
22
25
 
23
26
  def self.new params
24
- VideoConverter::Base.new params.deep_symbolize_keys
25
- end
26
-
27
- def self.find uid
28
- VideoConverter::Process.find uid
29
- end
30
- end
31
-
32
- Hash.class_eval do
33
- def deep_symbolize_keys
34
- inject({}) do |options, (key, value)|
35
- value = value.map { |v| v.is_a?(Hash) ? v.deep_symbolize_keys : v } if value.is_a? Array
36
- value = value.deep_symbolize_keys if value.is_a? Hash
37
- options[(key.to_sym rescue key) || key] = value
38
- options
39
- end
40
- end
41
-
42
- def deep_symbolize_keys!
43
- self.replace(self.deep_symbolize_keys)
27
+ VideoConverter::Base.new params.deep_symbolize_keys.deep_shellescape_values
44
28
  end
45
- end
29
+ end
@@ -0,0 +1,11 @@
1
+ class Array
2
+ def self.wrap(object)
3
+ if object.nil?
4
+ []
5
+ elsif object.respond_to?(:to_ary)
6
+ object.to_ary || [object]
7
+ else
8
+ [object]
9
+ end
10
+ end
11
+ end
@@ -2,68 +2,44 @@
2
2
 
3
3
  module VideoConverter
4
4
  class Base
5
- attr_accessor :input_array, :output_array, :log, :uid, :clear_tmp, :process
5
+ attr_accessor :uid, :outputs, :inputs, :clear_tmp
6
6
 
7
7
  def initialize params
8
8
  self.uid = params[:uid] || (Socket.gethostname + object_id.to_s)
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? }
12
- if params[:log].nil?
13
- self.log = '/dev/null'
14
- else
15
- self.log = params[:log]
16
- FileUtils.mkdir_p File.dirname(log)
9
+ self.outputs = Array.wrap(params[:output] || params[:outputs]).map do |output|
10
+ Output.new(output.merge(:uid => uid))
11
+ end
12
+ self.inputs = Array.wrap(params[:input] || params[:inputs]).map do |input|
13
+ Input.new(input, outputs)
17
14
  end
18
15
  self.clear_tmp = params[:clear_tmp].nil? ? true : params[:clear_tmp]
16
+ FileUtils.mkdir_p(File.dirname(VideoConverter.log))
19
17
  end
20
18
 
21
19
  def run
22
- self.process = VideoConverter::Process.new(uid, output_array)
23
- [:convert, :segment, :screenshot].each do |action|
24
- process.status = action.to_s
25
- process.status = "#{action}_error" and return false unless send(action)
26
- end
27
- process.status = 'finished'
28
- clear if clear_tmp
29
- true
20
+ success = true
21
+ [:convert, :segment, :screenshot].each { |action| success &&= send(action) }
22
+ clear if clear_tmp && success
23
+ success
30
24
  end
31
25
 
32
26
  private
33
27
 
34
28
  def convert
35
- params = {}
36
- [:input_array, :output_array, :log, :process].each do |param|
37
- params[param] = self.send(param)
38
- end
39
- Ffmpeg.new(params).run
29
+ Ffmpeg.new(inputs, outputs).run
40
30
  end
41
31
 
42
32
  def segment
43
- params = {}
44
- [:output_array, :log].each do |param|
45
- params[param] = self.send(param)
46
- end
47
- LiveSegmenter.new(params).run
33
+ LiveSegmenter.new(inputs, outputs).run
48
34
  end
49
35
 
50
36
  def screenshot
51
- output_array.outputs.each do |output|
52
- output.thumbnails.to_a.each do |thumbnail_params|
53
- VideoScreenshoter.new(thumbnail_params.merge(:ffmpeg => Ffmpeg.bin, :input => File.join(output.work_dir, output.filename.sub(/\.m3u8/, '.ts')), :output_dir => File.join(output.work_dir, 'thumbnails'))).make_screenshots
54
- end
55
- end
37
+ # TODO use VideoScreenshoter
56
38
  true
57
39
  end
58
40
 
59
41
  def clear
60
- output_array.outputs.each do |output|
61
- FileUtils.rm(Dir.glob(File.join(output.work_dir, '*.log')))
62
- FileUtils.rm(Dir.glob(File.join(output.work_dir, '*.log.mbtree')))
63
- FileUtils.rm(File.join(output.work_dir, output.filename.sub(/\.m3u8$/, '.ts'))) if output.type == :segmented
64
- end
65
- FileUtils.rm_r(process.process_dir)
66
- true
42
+ # TODO delete logs and ts file for segmented type if clear_tmp is true
67
43
  end
68
44
  end
69
45
  end
@@ -3,36 +3,21 @@
3
3
  module VideoConverter
4
4
  class Command
5
5
  class << self
6
- attr_accessor :debug, :verbose
6
+ attr_accessor :dry_run, :verbose
7
7
  end
8
- self.debug = false
8
+ self.dry_run = false
9
9
  self.verbose = false
10
10
 
11
11
  attr_accessor :command
12
12
 
13
13
  def initialize command, params = {}
14
- res = command.clone
15
- params.each do |param, value|
16
- value = value.to_s.strip
17
- res.gsub! "%{#{param}}", if value.empty?
18
- ''
19
- elsif matches = value.match(/^([\w-]+)\s+(.+)$/)
20
- "#{matches[1]} #{escape(matches[2])}"
21
- else
22
- escape(value)
23
- end
24
- end
25
- self.command = res
14
+ self.command = command.gsub(/%\{(\w+?)\}/) { |m| params[$1.to_sym] }
26
15
  raise ArgumentError.new("Command is not parsed '#{self.command}'") if self.command.match(/%{[\w\-.]+}/)
27
16
  end
28
17
 
29
- def escape value
30
- Shellwords.escape(value.to_s.gsub(/(?<!\\)(['+])/, '\\\\\1')).gsub("\\\\","\\")
31
- end
32
-
33
18
  def execute params = {}
34
19
  puts command if params[:verbose] || self.class.verbose
35
- if params[:debug] || self.class.debug
20
+ if params[:dry_run] || self.class.dry_run
36
21
  true
37
22
  else
38
23
  system command
@@ -3,99 +3,86 @@
3
3
  module VideoConverter
4
4
  class Ffmpeg
5
5
  class << self
6
- attr_accessor :bin, :one_pass, :paral, :log, :one_pass_command, :first_pass_command, :second_pass_command
6
+ attr_accessor :bin, :ffprobe_bin, :options, :one_pass_command, :first_pass_command, :second_pass_command
7
7
  end
8
8
 
9
9
  self.bin = '/usr/local/bin/ffmpeg'
10
- self.one_pass = false
11
- self.paral = true
12
- self.log = '/dev/null'
13
-
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 %{vf} %{frame_rate} -progress %{progressfile} -f mp4 %{local_path} 1>%{log} 2>&1 || exit 1"
10
+ self.ffprobe_bin = '/usr/local/bin/ffprobe'
11
+ self.options = {
12
+ :video_codec => '-c:v',
13
+ :audio_codec => '-c:a',
14
+ :keyframe_interval => '-g',
15
+ :passlogfile => '-passlogfile',
16
+ :video_bitrate => '-b:v',
17
+ :audio_bitrate => '-b:a',
18
+ :video_filter => '-vf',
19
+ :frame_rate => '-r',
20
+ :threads => '-threads',
21
+ :format => '-f',
22
+ :bitstream_format => '-bsf'
23
+ }
24
+ self.one_pass_command = '%{bin} -i %{input} -y %{options} %{output} 1>>%{log} 2>&1 || exit 1'
25
+ self.first_pass_command = '%{bin} -i %{input} -y -pass 1 -an -keyint_min 25 -pix_fmt yuv420p %{options} /dev/null 1>>%{log} 2>&1 || exit 1'
26
+ self.second_pass_command = '%{bin} -i %{input} -y -pass 2 -keyint_min 25 -pix_fmt yuv420p %{options} %{output} 1>>%{log} 2>&1 || exit 1'
15
27
 
16
- self.first_pass_command = "%{bin} -i %{input} -y -an -vcodec %{video_codec} -g %{keyframe_interval} -keyint_min 25 -pass 1 -passlogfile %{passlogfile} -progress %{progressfile} -b:v 3000k %{vf} %{frame_rate} -threads %{threads} -pix_fmt yuv420p -f mp4 /dev/null 1>>%{log} 2>&1 || exit 1"
28
+ attr_accessor :inputs, :outputs
17
29
 
18
- self.second_pass_command = "%{bin} -i %{input} -y -pass 2 -passlogfile %{passlogfile} -progress %{progressfile} -c:a %{audio_codec} -b:a %{audio_bitrate}k -ac 2 -c:v %{video_codec} -g %{keyframe_interval} -keyint_min 25 %{frame_rate} -b:v %{video_bitrate}k %{vf} -threads %{threads} -pix_fmt yuv420p -f mp4 %{local_path} 1>%{log} 2>&1 || exit 1"
19
-
20
- attr_accessor :input_array, :output_array, :one_pass, :paral, :log, :process
21
-
22
- def initialize params
23
- [:input_array, :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, :process].each do |param|
27
- self.send("#{param}=", params[param] ? params[param] : self.class.send(param))
28
- end
30
+ def initialize inputs, outputs
31
+ self.inputs = inputs
32
+ self.outputs = outputs
29
33
  end
30
34
 
35
+ # NOTE outputs of one group must have common first pass
31
36
  def run
32
- progress_thread = Thread.new { collect_progress }
33
- res = true
34
- input_array.inputs.each do |input|
35
- threads = []
36
- input.output_groups.each_with_index do |group, group_number|
37
- passlogfile = File.join(group.first.work_dir, "#{group_number}.log")
38
- progressfile = File.join(process.progress_dir, "#{group_number}.log")
39
- one_pass = self.one_pass || group.first.video_codec == 'copy'
40
- unless one_pass
41
- first_pass_command = Command.new self.class.first_pass_command, prepare_params(common_params.merge((group.first.playlist.to_hash rescue {})).merge(group.first.to_hash).merge(:passlogfile => passlogfile, :input => input, :progressfile => progressfile))
42
- res &&= first_pass_command.execute
37
+ success = true
38
+ threads = []
39
+ inputs.each do |input|
40
+ input.output_groups.each do |group|
41
+ qualities = group.select { |output| output.type != 'playlist' }
42
+ # NOTE if all qualities in group contain one_pass, use one_pass_command for ffmpeg
43
+ unless one_pass = qualities.inject { |r, q| r && q.one_pass }
44
+ # NOTE first pass is executed with maximal group's video bitrate
45
+ best_quality = qualities.sort { |q1, q2| q1.video_bitrate.to_i <=> q2.video_bitrate.to_i }.last
46
+ success &&= Command.new(self.class.first_pass_command, prepare_params(input, best_quality)).execute
43
47
  end
44
- group.each_with_index do |quality, quality_number|
45
- progressfile = File.join(process.progress_dir, "#{group_number}_#{quality_number}.log")
46
- if one_pass
47
- quality_command = Command.new self.class.one_pass_command, prepare_params(common_params.merge(quality.to_hash).merge(:passlogfile => passlogfile, :input => input, :progressfile => progressfile))
48
+ qualities.each do |output|
49
+ command = Command.new(one_pass ? self.class.one_pass_command : self.class.second_pass_command, prepare_params(input, output))
50
+ if VideoConverter.paral
51
+ threads << Thread.new { success &&= command.execute }
48
52
  else
49
- quality_command = Command.new self.class.second_pass_command, prepare_params(common_params.merge(quality.to_hash).merge(:passlogfile => passlogfile, :input => input, :progressfile => progressfile))
50
- end
51
- if paral
52
- threads << Thread.new { res &&= quality_command.execute }
53
- else
54
- res &&= quality_command.execute
53
+ success &&= command.execute
55
54
  end
56
55
  end
57
56
  end
58
- threads.each { |t| t.join } if paral
59
57
  end
60
- progress_thread.kill
61
- res
58
+ threads.each { |t| t.join } if VideoConverter.paral
59
+ success
62
60
  end
63
-
64
- private
65
61
 
66
- def common_params
67
- { :bin => self.class.bin, :log => log }
68
- end
62
+ private
69
63
 
70
- def prepare_params params
71
- width = params[:width] || params[:size].to_s.match(/^(\d+)x(\d*)$/).to_a[1] || 'trunc(oh*a/2)*2'
72
- height = params[:height] || params[:size].to_s.match(/^(\d*)x(\d+)$/).to_a[2] || 'trunc(ow/a/2)*2'
73
- if width.to_s.include?('trunc') && height.to_s.include?('trunc')
74
- params[:vf] = ''
75
- else
76
- params[:vf] = "-vf scale=#{width}:#{height}"
64
+ def prepare_params input, output
65
+ if output.size
66
+ output.video_filter = "scale=#{output.size.sub('x', ':')}"
67
+ elsif output.width && output.size
68
+ output.video_filter = "scale=#{output.width}:#{output.height}"
69
+ elsif output.width
70
+ output.video_filter = "scale=#{output.width}:trunc\\(ow/a/2\\)*2"
71
+ elsif output.height
72
+ output.video_filter = "scale=trunc\\(oh*a/2\\)*2:#{output.height}"
77
73
  end
78
- params[:frame_rate] = params[:frame_rate] ? "-r #{params[:frame_rate]}" : ''
79
- params
80
- end
81
74
 
82
- def processes_count
83
- (one_pass ? 0 : input_array.inputs.inject(0) { |sum, i| sum + i.output_groups.count }) +
84
- input_array.inputs.map { |i| i.output_groups.map { |g| g.count } }.flatten.inject(:+)
85
- end
86
-
87
- def collect_progress
88
- duration = input_array.inputs.first.metadata[:duration_in_ms]
89
- loop do
90
- sleep Process.collect_progress_interval
91
- koefs = []
92
- Dir.glob(File.join(process.progress_dir, '*.log')).each do |progressfile|
93
- if matches = `tail -n 9 #{progressfile}`.match(/out_time_ms=(\d+).*?progress=(\w+)/m)
94
- koefs << (matches[2] == 'end' ? 1 : (matches[1].to_f / 1000 / duration))
75
+ {
76
+ :bin => self.class.bin,
77
+ :input => input.to_s,
78
+ :log => VideoConverter.log,
79
+ :output => output.ffmpeg_output,
80
+ :options => self.class.options.map do |output_option, ffmpeg_option|
81
+ if output.send(output_option).present?
82
+ ffmpeg_option += ' ' + output.send(output_option).to_s
95
83
  end
96
- end
97
- process.collect_progress(koefs.inject { |sum, k| sum + k } / processes_count)
98
- end
84
+ end.join(' ')
85
+ }
99
86
  end
100
87
  end
101
88
  end
@@ -0,0 +1,32 @@
1
+ class Hash
2
+ def deep_symbolize_keys
3
+ inject({}) do |options, (key, value)|
4
+ value = value.map { |v| v.is_a?(Hash) ? v.deep_symbolize_keys : v } if value.is_a? Array
5
+ value = value.deep_symbolize_keys if value.is_a? Hash
6
+ options[(key.to_sym rescue key) || key] = value
7
+ options
8
+ end
9
+ end
10
+
11
+ def deep_symbolize_keys!
12
+ self.replace(self.deep_symbolize_keys)
13
+ end
14
+
15
+ def deep_shellescape_values
16
+ inject({}) do |options, (key, value)|
17
+ if value.is_a? Array
18
+ value = value.map { |v| v.is_a?(Hash) ? v.deep_shellescape_values : v.shellescape }
19
+ elsif value.is_a? Hash
20
+ value = value.deep_shellescape_values
21
+ else
22
+ value = value.to_s.shellescape
23
+ end
24
+ options[key] = value
25
+ options
26
+ end
27
+ end
28
+
29
+ def deep_shellescape_values!
30
+ self.replace(self.deep_shellescape_values)
31
+ end
32
+ end
@@ -6,19 +6,28 @@ module VideoConverter
6
6
  attr_accessor :metadata_command
7
7
  end
8
8
 
9
- self.metadata_command = "%{bin} -i %{input} 2>&1"
9
+ self.metadata_command = "%{ffprobe_bin} %{input} 2>&1"
10
10
 
11
- attr_accessor :input, :outputs, :output_groups
11
+ attr_accessor :input, :output_groups
12
12
 
13
- def initialize input
14
- raise ArgumentError.new('input is needed') if input.nil? || input.empty?
13
+ def initialize input, outputs = []
14
+ raise ArgumentError.new('input is needed') if input.blank?
15
15
  self.input = input
16
- self.outputs = []
16
+ raise ArgumentError.new("#{input} does not exist") unless exists?
17
+
17
18
  self.output_groups = []
19
+ outputs.select { |output| output.type == 'playlist' }.each_with_index do |playlist, index|
20
+ paths = playlist.streams.map { |stream| stream[:path] }
21
+ output_group = outputs.select { |output| paths.include?(output.filename) && (!output.path || output.path == input.to_s) }
22
+ if output_group.any?
23
+ output_group.each { |output| output.passlogfile = File.join(output.work_dir, "group#{index}.log") }
24
+ self.output_groups << output_group.unshift(playlist)
25
+ end
26
+ end
18
27
  end
19
28
 
20
29
  def to_s
21
- Shellwords.escape(input).gsub("\\\\","\\")
30
+ input
22
31
  end
23
32
 
24
33
  def exists?
@@ -28,16 +37,14 @@ module VideoConverter
28
37
  response = http.request_head url.path
29
38
  Net::HTTPSuccess === response
30
39
  end
31
- elsif is_local?
32
- File.file? input
33
40
  else
34
- false
41
+ is_local?
35
42
  end
36
43
  end
37
44
 
38
45
  def metadata
39
46
  metadata = {}
40
- s = `#{Command.new self.class.metadata_command, common_params}`.encode!('UTF-8', 'UTF-8', :invalid => :replace)
47
+ s = `#{Command.new self.class.metadata_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input}`.encode!('UTF-8', 'UTF-8', :invalid => :replace)
41
48
  if (m = s.match(/Stream.*?Audio:\s*(\w+).*?(\d+)\s*Hz.*?(\d+)\s*kb\/s.*?$/).to_a).any?
42
49
  metadata[:audio_codec] = m[1]
43
50
  metadata[:audio_sample_rate] = m[2].to_i
@@ -65,7 +72,7 @@ module VideoConverter
65
72
  metadata[:file_size_in_bytes] = response['content-length'].to_i
66
73
  end
67
74
  elsif is_local?
68
- metadata[:file_size_in_bytes] = File.size(input)
75
+ metadata[:file_size_in_bytes] = File.size(input.gsub('\\', ''))
69
76
  end
70
77
  metadata[:format] = File.extname(input).sub('.', '')
71
78
  end
@@ -77,13 +84,8 @@ module VideoConverter
77
84
  end
78
85
 
79
86
  def is_local?
80
- File.file?(input)
81
- end
82
-
83
- private
84
-
85
- def common_params
86
- { :bin => VideoConverter::Ffmpeg.bin, :input => input }
87
+ # NOTE method file escapes himself
88
+ File.file?(input.gsub('\\', ''))
87
89
  end
88
90
  end
89
91
  end
@@ -3,70 +3,69 @@
3
3
  module VideoConverter
4
4
  class LiveSegmenter
5
5
  class << self
6
- attr_accessor :bin, :ffprobe_bin, :chunks_command, :chunk_prefix, :encoding_profile, :log, :paral, :select_streams
6
+ attr_accessor :bin, :segment_seconds, :chunk_prefix, :encoding_profile, :select_streams, :command
7
7
  end
8
8
 
9
9
  self.bin = '/usr/local/bin/live_segmenter'
10
- self.ffprobe_bin = '/usr/local/bin/ffprobe'
10
+ self.segment_seconds = 4
11
11
  self.chunk_prefix = 's'
12
12
  self.encoding_profile = 's'
13
- self.log = '/dev/null'
14
- self.paral = true
15
13
  self.select_streams = 'v'
16
14
 
17
- self.chunks_command = '%{ffmpeg_bin} -f mp4 -i %{local_path} -vcodec copy -acodec copy -f mpegts -bsf h264_mp4toannexb pipe:1 2>>/dev/null | %{bin} %{segment_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
15
+ self.command = '%{ffmpeg_bin} -i %{ffmpeg_output} -vcodec copy -acodec copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{segment_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
18
16
 
19
- attr_accessor :paral, :chunk_prefix, :encoding_profile, :log, :output_array
17
+ attr_accessor :inputs, :outputs
20
18
 
21
- def initialize params
22
- self.output_array = params[:output_array] or raise ArgumentError.new("output_array is needed")
23
- [:chunk_prefix, :encoding_profile, :log, :paral].each do |param|
24
- self.send("#{param}=", params[param].nil? ? self.class.send(param) : params[param])
25
- end
19
+ def initialize inputs, outputs
20
+ self.inputs = inputs
21
+ self.outputs = outputs
26
22
  end
27
23
 
28
24
  def run
29
- res = true
25
+ success = true
30
26
  threads = []
31
27
  p = Proc.new do |output|
32
28
  make_chunks(output) && gen_quality_playlist(output)
33
29
  end
34
- output_array.playlists.each do |playlist|
35
- playlist.items.each do |item|
36
- if paral
37
- threads << Thread.new { res &&= p.call(item) }
38
- else
39
- res &&= p.call(item)
30
+ inputs.each do |input|
31
+ input.output_groups.each do |group|
32
+ group.select { |output| output.type != 'playlist' }.each do |output|
33
+ if VideoConverter.paral
34
+ threads << Thread.new { success &&= p.call(output) }
35
+ else
36
+ success &&= p.call(output)
37
+ end
40
38
  end
39
+ success &&= gen_group_playlist(group.detect { |output| output.type == 'playlist' })
41
40
  end
42
- res &&= gen_group_playlist playlist
43
41
  end
44
- threads.each { |t| t.join } if paral
45
- res
42
+ threads.each { |t| t.join } if VideoConverter.paral
43
+ success
46
44
  end
47
45
 
48
46
  private
49
47
 
50
48
  def make_chunks output
51
- Command.new(self.class.chunks_command, common_params.merge(output.to_hash)).execute
49
+ Command.new(self.class.command, prepare_params(output)).execute
52
50
  end
53
51
 
54
52
  def gen_quality_playlist output
55
53
  res = ''
56
54
  durations = []
57
55
  # order desc
58
- chunks = Dir::glob(File.join(output.chunks_dir, "#{chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
56
+ chunks = Dir::glob(File.join(output.chunks_dir, "#{self.class.chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
59
57
  File.basename(c2).match(/\d+/).to_s.to_i <=> File.basename(c1).match(/\d+/).to_s.to_i
60
58
  end
61
59
  # chunk duration = (pts of first frame of the next chunk - pts of first frame of current chunk) / time_base
62
60
  # for the last chunks the last two pts are used
63
- prl_pts, l_pts = `#{self.class.ffprobe_bin} -show_frames -select_streams #{self.class.select_streams} -print_format csv -loglevel fatal #{chunks.first} | tail -n2 2>&1`.split("\n").map { |l| l.split(',')[3].to_i }
61
+ prl_pts, l_pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{self.class.select_streams} -print_format csv -loglevel fatal #{chunks.first} | tail -n2 2>&1`.split("\n").map { |l| l.split(',')[3].to_i }
62
+ # NOTE for case when chunk has one frame
63
+ l_pts ||= prl_pts
64
64
  next_chunk_pts = 2 * l_pts - prl_pts
65
- time_base = `#{self.class.ffprobe_bin} -show_streams -select_streams #{self.class.select_streams} -loglevel fatal #{chunks.first} 2>&1`.match(/\ntime_base=1\/(\d+)/)[1].to_f
65
+ time_base = `#{Ffmpeg.ffprobe_bin} -show_streams -select_streams #{self.class.select_streams} -loglevel fatal #{chunks.first} 2>&1`.match(/\ntime_base=1\/(\d+)/)[1].to_f
66
66
  chunks.each do |chunk|
67
- pts = `#{self.class.ffprobe_bin} -show_frames -select_streams #{self.class.select_streams} -print_format csv -loglevel fatal #{chunk} | head -n1 2>&1`.split(',')[3].to_i
67
+ pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{self.class.select_streams} -print_format csv -loglevel fatal #{chunk} | head -n1 2>&1`.split(',')[3].to_i
68
68
  durations << (duration = (next_chunk_pts - pts) / time_base)
69
- puts "chunk: #{chunk}, duration: #{duration}, next_chunk_pts: #{next_chunk_pts}, pts: #{pts}"
70
69
  next_chunk_pts = pts
71
70
  res = File.join(File.basename(output.chunks_dir), File.basename(chunk)) + "\n" + res
72
71
  res = "#EXTINF:%0.2f,\n" % duration + res
@@ -86,8 +85,17 @@ module VideoConverter
86
85
  true
87
86
  end
88
87
 
89
- def common_params
90
- { :ffmpeg_bin => Ffmpeg.bin, :bin => self.class.bin, :log => log, :chunk_prefix => chunk_prefix, :encoding_profile => encoding_profile }
88
+ def prepare_params output
89
+ {
90
+ :ffmpeg_bin => Ffmpeg.bin,
91
+ :ffmpeg_output => output.ffmpeg_output,
92
+ :bin => self.class.bin,
93
+ :segment_seconds => self.class.segment_seconds,
94
+ :chunks_dir => output.chunks_dir,
95
+ :chunk_prefix => self.class.chunk_prefix,
96
+ :encoding_profile => self.class.encoding_profile,
97
+ :log => VideoConverter.log
98
+ }
91
99
  end
92
100
  end
93
- end
101
+ end
@@ -0,0 +1,9 @@
1
+ class Object
2
+ def blank?
3
+ respond_to?(:empty?) ? empty? : !self
4
+ end
5
+
6
+ def present?
7
+ !blank?
8
+ end
9
+ end
@@ -3,67 +3,65 @@
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, :video_codec, :audio_codec
6
+ attr_accessor :work_dir, :keyframe_interval, :threads, :video_codec, :audio_codec
7
7
  end
8
8
 
9
- self.base_url = '/tmp'
10
9
  self.work_dir = '/tmp'
11
- self.video_bitrate = 700
12
- self.audio_bitrate = 200
13
- self.segment_seconds = 10
14
10
  self.keyframe_interval = 250
15
11
  self.threads = 1
16
12
  self.video_codec = 'libx264'
17
13
  self.audio_codec = 'libfaac'
18
14
 
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, :thumbnails, :frame_rate, :size, :width, :height
15
+ attr_accessor :uid, :work_dir, :threads, :passlogfile
16
+ attr_accessor :type, :filename
17
+ attr_accessor :format, :ffmpeg_output, :video_codec, :audio_codec, :bitstream_format
18
+ attr_accessor :one_pass, :video_bitrate, :audio_bitrate
19
+ attr_accessor :streams, :path, :chunks_dir
20
+ attr_accessor :keyframe_interval, :frame_rate
21
+ attr_accessor :size, :width, :height, :video_filter
22
+ attr_accessor :thumbnails
23
+
20
24
 
21
25
  def initialize params = {}
26
+ # Inner
22
27
  self.uid = params[:uid].to_s
28
+ self.work_dir = File.join(self.class.work_dir, uid)
29
+ FileUtils.mkdir_p(work_dir)
30
+ self.threads = params[:threads] || self.class.threads
23
31
 
24
32
  # General output options
25
- self.type = params[:type] ? params[:type].to_sym : :standard
26
- raise ArgumentError.new('Incorrect type') unless %w(standard segmented playlist transfer-only).include?(type.to_s)
27
-
28
- self.format = params[:format]
29
- if !format && params[:filename]
30
- self.format = File.extname(params[:filename]).sub('.', '')
31
- end
32
- if format == 'm3u8' || !format && type == :segmented
33
- self.format = 'ts'
33
+ self.type = params[:type]
34
+ raise ArgumentError.new('Incorrect type') if type && !%w(default segmented playlist).include?(type)
35
+ self.filename = params[:filename] or raise ArgumentError.new('filename required')
36
+
37
+ # Formats and codecs
38
+ if type == 'segmented'
39
+ self.format = 'mpegts'
40
+ self.ffmpeg_output = File.join(work_dir, File.basename(filename, '.*') + '.ts')
41
+ else
42
+ self.format = File.extname(filename).sub('.', '')
43
+ self.ffmpeg_output = File.join(work_dir, filename)
34
44
  end
35
- self.format = 'mp4' if format.nil? || format.empty?
36
- 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)
37
-
38
- self.base_url = (params[:url] ? File.dirname(params[:url]) : params[:base_url]) || self.class.base_url
39
- self.filename = (params[:url] ? File.basename(params[:url]) : params[:filename]) || self.uid + '.' + self.format
40
- self.url = params[:url] ? params[:url] : File.join(base_url, filename)
41
- self.work_dir = File.join(params[:work_dir] || self.class.work_dir, uid)
42
- format_regexp = Regexp.new("#{File.extname(filename)}$")
43
- self.local_path = File.join(work_dir, filename.sub(format_regexp, ".#{format}"))
44
- FileUtils.mkdir_p File.dirname(local_path)
45
- if type == :segmented
46
- self.chunks_dir = File.join(work_dir, filename.sub(format_regexp, ''))
47
- FileUtils.mkdir_p chunks_dir
48
- end
49
- self.threads = self.class.threads
50
- self.path = params[:path]
45
+ self.video_codec = params[:video_codec] || self.class.video_codec
46
+ self.audio_codec = params[:audio_codec] || self.class.audio_codec
47
+ self.bitstream_format = params[:bitstream_format]
51
48
 
52
49
  # Rate controle
53
- self.video_bitrate = params[:video_bitrate].to_i > 0 ? params[:video_bitrate].to_i : self.class.video_bitrate
54
- self.audio_bitrate = params[:audio_bitrate].to_i > 0 ? params[:audio_bitrate].to_i : self.class.audio_bitrate
50
+ self.one_pass = !!params[:one_pass]
51
+ self.video_bitrate = "#{params[:video_bitrate]}k" if params[:video_bitrate]
52
+ self.audio_bitrate = "#{params[:audio_bitrate]}k" if params[:audio_bitrate]
55
53
 
56
54
  # Segmented streaming
57
- self.streams = params[:streams].to_a
58
- self.segment_seconds = params[:segment_seconds] || self.class.segment_seconds
55
+ self.streams = params[:streams]
56
+ self.path = params[:path]
57
+ if type == 'segmented'
58
+ self.chunks_dir = File.join(work_dir, File.basename(filename, '.*'))
59
+ FileUtils.mkdir_p(chunks_dir)
60
+ end
59
61
 
60
62
  # Frame rate
61
- self.keyframe_interval = params[:keyframe_interval].to_i > 0 ? params[:keyframe_interval].to_i : self.class.keyframe_interval
62
- self.frame_rate = params[:frame_rate].to_i if params[:frame_rate]
63
-
64
- # Format and codecs
65
- self.video_codec = (params[:copy_video] ? 'copy' : params[:video_codec]) || self.class.video_codec
66
- self.audio_codec = (params[:copy_audio] ? 'copy' : params[:audio_codec]) || self.class.audio_codec
63
+ self.keyframe_interval = params[:keyframe_interval] || self.class.keyframe_interval
64
+ self.frame_rate = params[:frame_rate]
67
65
 
68
66
  # Resolution
69
67
  self.size = params[:size]
@@ -73,24 +71,5 @@ module VideoConverter
73
71
  #Thumbnails
74
72
  self.thumbnails = params[:thumbnails]
75
73
  end
76
-
77
- def to_hash
78
- keys = [:video_bitrate, :local_path, :segment_seconds, :chunks_dir, :audio_bitrate, :keyframe_interval, :frame_rate, :threads, :video_codec, :audio_codec, :size, :width, :height]
79
- Hash[*keys.map{ |key| [key, self.send(key)] }.flatten]
80
- end
81
-
82
- def self.outputs output
83
- (output.is_a?(Hash) ? [output] : output).map { |output| output.is_a?(self.class) ? output : new(output) }
84
- end
85
-
86
- def self.playlists output
87
- outputs(output).select { |output| output.type == :playlist }
88
- end
89
-
90
- def qualities output
91
- raise TypeError.new('Only for playlists') unless type == :playlist
92
- stream_paths = streams.map { |stream| stream[:path] }
93
- self.class.outputs(output).select { |output| stream_paths.include? output.filename }
94
- end
95
74
  end
96
75
  end
@@ -1,3 +1,3 @@
1
1
  module VideoConverter
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ #EXTM3U
2
+ #EXT-X-TARGETDURATION:10
3
+ #EXT-X-MEDIA-SEQUENCE:0
4
+ #EXTINF:10, no desc
5
+ ./chunk0.ts
6
+ #EXTINF:10, no desc
7
+ ./chunk1.ts
8
+ #EXT-X-ENDLIST
data/test/hls_test.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class HlsTest < Test::Unit::TestCase
4
+ setup do
5
+ @concat = "/tmp/test_concat.mp4"
6
+ VideoConverter::Hls.new({:input=>"test/fixtures/test_playlist.m3u8", :output=>@concat}).concat
7
+ end
8
+
9
+ should 'concat' do
10
+ metadata = VideoConverter.new(:input => @concat).inputs.first.metadata
11
+ assert metadata[:duration_in_ms] > 20_000
12
+ assert_equal 'mp4', metadata[:format]
13
+ end
14
+ end
@@ -1,138 +1,70 @@
1
1
  require 'test_helper'
2
2
 
3
3
  class VideoConverterTest < Test::Unit::TestCase
4
- context 'run' do
5
- setup do
6
- @input_file = 'test/fixtures/test.mp4'
7
- @input_url = 'http://techslides.com/demos/sample-videos/small.mp4'
8
- end
4
+ setup do
5
+ VideoConverter::Command.verbose = true
6
+ end
9
7
 
10
- context 'with default type' do
8
+ context 'segmentation' do
9
+ context 'with transcoding' do
11
10
  setup do
12
- @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', :clear_tmp => false)
13
- @res = @c.run
14
- end
15
- should 'convert files' do
16
- 2.times do |n|
17
- file = File.join(VideoConverter::Output.work_dir, @c.uid, "tmp/test#{n + 1}.mp4")
18
- assert File.exists?(file)
19
- assert File.size(file) > 0
20
- end
21
- end
22
- should 'return success convert process' do
23
- assert VideoConverter.find(@c.uid)
24
- assert @res
25
- assert_equal 'finished', VideoConverter.find(@c.uid).status
26
- end
27
- should 'write log file' do
28
- assert File.exists?('tmp/test.log')
29
- assert !File.read('tmp/test.log').empty?
11
+ VideoConverter.paral = true
12
+ (@c = VideoConverter.new(
13
+ "input"=>["test/fixtures/test (1).mp4"],
14
+ "output"=>[
15
+ {"video_bitrate"=>676, "filename"=>"sd1.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
16
+ {"video_bitrate"=>1172, "filename"=>"sd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>528},
17
+ {"filename"=>"playlist.m3u8", "type"=>"playlist", "streams"=>[
18
+ {"path"=>"sd1.m3u8", "bandwidth"=>804}, {"path"=>"sd2.m3u8", "bandwidth"=>1300}
19
+ ]},
20
+ {"video_bitrate"=>1550, "filename"=>"hd1.m3u8", "type"=>"segmented", "audio_bitrate"=>48, "height"=>720},
21
+ {"video_bitrate"=>3200, "filename"=>"hd2.m3u8", "type"=>"segmented", "audio_bitrate"=>128, "height"=>720},
22
+ {"filename"=>"hd_playlist.m3u8", "type"=>"playlist", "streams"=>[
23
+ {"path"=>"hd1.m3u8", "bandwidth"=>1598}, {"path"=>"hd2.m3u8", "bandwidth"=>3328}
24
+ ]}
25
+ ]
26
+ )).run
30
27
  end
31
- end
32
28
 
33
- context 'with type segmented' do
34
- setup do
35
- @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'}])
36
- @res = @c.run
37
- @work_dir = File.join(VideoConverter::Output.work_dir, @c.uid)
38
- 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
44
- end
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')
49
- end
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
66
- should 'clear tmp files' do
67
- [200, 300, 500, 700].each do |q|
68
- assert !File.exists?(File.join(@work_dir, "r#{q}.ts"))
29
+ should 'generate hls' do
30
+ %w(sd1 sd2 hd1 hd2).each do |quality|
31
+ # should create chunks
32
+ assert_equal ['s-00001.ts', 's-00002.ts'], Dir.entries(File.join(VideoConverter::Output.work_dir, @c.uid, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
33
+ # TODO verify that chunks have different quality (weight)
34
+ # should create playlists
35
+ assert File.exists?(playlist = File.join(VideoConverter::Output.work_dir, @c.uid, "#{quality}.m3u8"))
36
+ # TODO verify that playlist is valid (contain all chunks and modifiers)
69
37
  end
70
38
  end
71
39
  end
72
40
 
73
- context 'only segment some files' do
41
+ context 'without transcoding' do
74
42
  setup do
75
- # prepare files
76
- @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'}])
77
- @c1.run
78
- @input1, @input2, @input3, @input4 = @c1.output_array.outputs.map { |output| output.local_path }
79
- # test segmentation
80
- @c2 = VideoConverter.new(:input => [@input1, @input2, @input3, @input4], :output => [
81
- {:path => @input1, :type => :segmented, :filename => '1.m3u8', :copy_video => true},
82
- {:path => @input2, :type => :segmented, :filename => '2.m3u8', :copy_video => true},
83
- {:path => @input3, :type => :segmented, :filename => '3.m3u8', :copy_video => true},
84
- {:path => @input4, :type => :segmented, :filename => '4.m3u8', :copy_video => true},
85
- {:type => :playlist, :streams => [{:path => '1.m3u8'}, {:path => '2.m3u8'}], :filename => 'playlist1.m3u8'},
86
- {:type => :playlist, :streams => [{:path => '3.m3u8'}, {:path => '4.m3u8'}], :filename => 'playlist2.m3u8'}
87
- ], :clear_tmp => false)
88
- @res = @c2.run
89
- @work_dir = File.join(VideoConverter::Output.work_dir, @c2.uid)
90
- end
91
- should 'create chunks' do
92
- 4.times do |n|
93
- assert Dir.entries(File.join(@work_dir, "#{n + 1}")).count > 0
94
- end
95
- end
96
- should 'create quality playlists' do
97
- 4.times do |n|
98
- playlist = "#{n + 1}.m3u8"
99
- assert File.exists?(File.join(@work_dir, playlist))
100
- assert File.read(File.join(@work_dir, playlist)).include?('s-00001')
101
- end
102
- end
103
- should 'create group playlist' do
104
- playlist = File.join(@work_dir, 'playlist1.m3u8')
105
- assert File.exists?(playlist)
106
- assert File.read(playlist).include?('1.m3u8')
107
- assert File.read(playlist).include?('2.m3u8')
108
-
109
- playlist = File.join(@work_dir, 'playlist2.m3u8')
110
- assert File.exists?(playlist)
111
- assert File.read(playlist).include?('3.m3u8')
112
- assert File.read(playlist).include?('4.m3u8')
113
- end
114
- should 'not convert inputs' do
115
- 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]
116
- 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]
117
- 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]
118
- 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]
43
+ VideoConverter.paral = false
44
+ FileUtils.cp("test/fixtures/test (1).mp4", "test/fixtures/test (2).mp4")
45
+ (@c = VideoConverter.new(
46
+ :input => ["test/fixtures/test (1).mp4", "test/fixtures/test (2).mp4"],
47
+ :output => [
48
+ {:filename=>"q1.m3u8", :path=>"test/fixtures/test (1).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", :bitstream_format=>"h264_mp4toannexb"},
49
+ {:filename=>"q2.m3u8", :path=>"test/fixtures/test (2).mp4", :type=>"segmented", :one_pass=>true, :video_codec=>"copy", :audio_codec=>"copy", :bitstream_format=>"h264_mp4toannexb"},
50
+ {:filename=>"playlist.m3u8", :type=>"playlist", :streams=>[
51
+ {:path=>"q1.m3u8", :bandwidth=>464}, {:path=>"q2.m3u8", :bandwidth=>928}
52
+ ]}
53
+ ]
54
+ )).run
55
+ FileUtils.rm("test/fixtures/test (2).mp4")
119
56
  end
120
- end
121
57
 
122
- context 'thumbnails' do
123
- setup do
124
- @c = VideoConverter.new(:input => @input_file, :output => [{:filename => 'test1.mp4'}, {:filename => 'test2.mp4', :thumbnails => [{:times => [1,'50%',-1]}]}])
125
- @c.run
126
- end
127
- should 'create thumbnails' do
128
- 3.times do |n|
129
- assert File.exists? File.join(@c.output_array.outputs.first.work_dir, 'thumbnails', "scr00#{n + 1}.jpg")
58
+ should 'generate hls' do
59
+ %w(q1 q2).each do |quality|
60
+ # should create chunks
61
+ assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(VideoConverter::Output.work_dir, @c.uid, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
62
+ # TODO verify that chunks have the same quality (weight)
63
+ # should create playlists
64
+ assert File.exists?(playlist = File.join(VideoConverter::Output.work_dir, @c.uid, "#{quality}.m3u8"))
65
+ # TODO verify that playlist is valid (contain all chunks and modifiers)
130
66
  end
131
67
  end
132
- should 'convert outputs' do
133
- assert File.exists? File.join(@c.output_array.outputs.first.work_dir, 'test1.mp4')
134
- assert File.exists? File.join(@c.output_array.outputs.first.work_dir, 'test2.mp4')
135
- end
136
68
  end
137
69
  end
138
70
  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.3.2
4
+ version: 0.4.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: 2014-03-25 00:00:00.000000000 Z
12
+ date: 2014-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: video_screenshoter
@@ -106,19 +106,22 @@ files:
106
106
  - README.md
107
107
  - Rakefile
108
108
  - lib/video_converter.rb
109
+ - lib/video_converter/array.rb
109
110
  - lib/video_converter/base.rb
110
111
  - lib/video_converter/command.rb
111
112
  - lib/video_converter/ffmpeg.rb
113
+ - lib/video_converter/hash.rb
112
114
  - lib/video_converter/hls.rb
113
115
  - lib/video_converter/input.rb
114
- - lib/video_converter/input_array.rb
115
116
  - lib/video_converter/live_segmenter.rb
117
+ - lib/video_converter/object.rb
116
118
  - lib/video_converter/output.rb
117
- - lib/video_converter/output_array.rb
118
- - lib/video_converter/process.rb
119
119
  - lib/video_converter/version.rb
120
- - test/fixtures/test.mp4
121
- - test/input_test.rb
120
+ - test/fixtures/chunk0.ts
121
+ - test/fixtures/chunk1.ts
122
+ - test/fixtures/test (1).mp4
123
+ - test/fixtures/test_playlist.m3u8
124
+ - test/hls_test.rb
122
125
  - test/test_helper.rb
123
126
  - test/video_converter_test.rb
124
127
  - video_converter.gemspec
@@ -137,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
137
140
  version: '0'
138
141
  segments:
139
142
  - 0
140
- hash: -2052745333442721331
143
+ hash: -3922597022375012517
141
144
  required_rubygems_version: !ruby/object:Gem::Requirement
142
145
  none: false
143
146
  requirements:
@@ -146,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
149
  version: '0'
147
150
  segments:
148
151
  - 0
149
- hash: -2052745333442721331
152
+ hash: -3922597022375012517
150
153
  requirements:
151
154
  - ffmpeg, version 1.2 or greated configured with libx264 and libfaac
152
155
  - live_segmenter to convert to hls
@@ -156,7 +159,10 @@ signing_key:
156
159
  specification_version: 3
157
160
  summary: Ffmpeg, mencoder based converter to mp4, m3u8
158
161
  test_files:
159
- - test/fixtures/test.mp4
160
- - test/input_test.rb
162
+ - test/fixtures/chunk0.ts
163
+ - test/fixtures/chunk1.ts
164
+ - test/fixtures/test (1).mp4
165
+ - test/fixtures/test_playlist.m3u8
166
+ - test/hls_test.rb
161
167
  - test/test_helper.rb
162
168
  - test/video_converter_test.rb
@@ -1,24 +0,0 @@
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 == Shellwords.escape(output.path.to_s).gsub("\\\\","\\") }.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
@@ -1,21 +0,0 @@
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
- end
21
- end
@@ -1,82 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module VideoConverter
4
- class Process
5
- attr_accessor :uid, :status, :progress, :status_progress, :pid, :process_dir, :status_progress_koefs
6
-
7
- class << self
8
- attr_accessor :process_dir, :collect_progress_interval
9
- end
10
- self.process_dir = '/tmp/video_converter_process'
11
- self.collect_progress_interval = 10
12
-
13
- def self.find uid
14
- if Dir.exists?(File.join(process_dir, uid.to_s))
15
- new uid
16
- else
17
- nil
18
- end
19
- end
20
-
21
- def initialize uid, output_array = nil
22
- self.uid = uid.to_s
23
- self.process_dir = File.join(self.class.process_dir, uid)
24
-
25
- unless Dir.exists? process_dir
26
- FileUtils.mkdir_p process_dir
27
- self.pid = `cat /proc/self/stat`.split[3]
28
- self.status = 'started'
29
- self.progress = self.status_progress = 0
30
- end
31
-
32
- if output_array
33
- self.status_progress_koefs = {}
34
- self.status_progress_koefs[:screenshot] = 0.05 if output_array.outputs.detect { |o| o.thumbnails }
35
- self.status_progress_koefs[:segment] = 0.1 if output_array.playlists.any?
36
- self.status_progress_koefs[:convert] = 1 - status_progress_koefs.values.inject(0, :+)
37
- else
38
- self.status_progress_koefs = { :screenshot => 0.05, :segment => 0.1, :convert => 0.85 }
39
- end
40
- end
41
-
42
- # attr are saved in local file
43
- def get_attr attr
44
- File.open(File.join(process_dir, "#{attr}.txt"), 'r') { |f| f.read } rescue nil
45
- end
46
- def set_attr attr, value
47
- File.open(File.join(process_dir, "#{attr}.txt"), 'w') { |f| f.write value }
48
- end
49
-
50
- [:progress, :status_progress, :pid].each do |attr|
51
- define_method attr do
52
- get_attr attr
53
- end
54
- define_method "#{attr}=" do |value|
55
- set_attr attr, value
56
- end
57
- end
58
-
59
- def status
60
- get_attr :status
61
- end
62
-
63
- def status= value
64
- set_attr :status, value
65
- FileUtils.mkdir_p progress_dir if %w(convert).include?(value.to_s)
66
- self.progress = self.status_progress = 1 if value.to_s == 'finished'
67
- end
68
-
69
- def stop
70
- `pkill -P #{pid}`
71
- end
72
-
73
- def collect_progress progress
74
- self.status_progress = progress
75
- self.progress = progress * status_progress_koefs[status.to_sym].to_f
76
- end
77
-
78
- def progress_dir
79
- File.join process_dir, 'progress', status
80
- end
81
- end
82
- end
Binary file
data/test/input_test.rb DELETED
@@ -1,73 +0,0 @@
1
- require 'test_helper'
2
-
3
- class InputTest < Test::Unit::TestCase
4
- setup do
5
- @test_url = 'http://techslides.com/demos/sample-videos/small.mp4'
6
- end
7
-
8
- context 'exists' do
9
- context 'when it is local' do
10
- context 'and exists' do
11
- should 'return true' do
12
- assert VideoConverter::Input.new('test/fixtures/test.mp4').exists?
13
- end
14
- end
15
-
16
- context 'and does not exist or is not file' do
17
- should 'return false' do
18
- assert !VideoConverter::Input.new('test/fixtures/not_existed_file').exists?
19
- assert !VideoConverter::Input.new('test/fixtures').exists?
20
- end
21
- end
22
- end
23
-
24
- context 'when it is http' do
25
- context 'and exists' do
26
- should 'return true' do
27
- assert VideoConverter::Input.new(@test_url)
28
- end
29
- end
30
-
31
- context 'when does not exist or is not file' do
32
- should 'return false' do
33
- assert !VideoConverter::Input.new(@test_url + 'any').exists?
34
- assert !VideoConverter::Input.new(URI.parse(@test_url).host).exists?
35
- end
36
- end
37
- end
38
- end
39
-
40
- context 'metadata' do
41
- context 'when input does not exist' do
42
- should 'be empty' do
43
- assert VideoConverter::Input.new('test/fixtures/not_existed_file').metadata.empty?
44
- end
45
- end
46
-
47
- context 'when input is dir' do
48
- should 'be empty' do
49
- assert VideoConverter::Input.new('tmp/fixtures').metadata.empty?
50
- end
51
- end
52
-
53
- context 'when input is not video file' do
54
- should 'be empty' do
55
- assert VideoConverter::Input.new(__FILE__).metadata.empty?
56
- end
57
- end
58
-
59
- context 'when input is video file' do
60
- should 'not be empty' do
61
- h = {:audio_bitrate_in_kbps=>97, :audio_codec=>"aac", :audio_sample_rate=>44100, :channels=>2, :duration_in_ms=>4019, :total_bitrate_in_kbps=>1123, :frame_rate=>29.97, :height=>360, :video_bitrate_in_kbps=>1020, :video_codec=>"h264", :width=>640, :file_size_in_bytes=>564356, :format=>"mp4"}
62
- assert_equal h, VideoConverter::Input.new('test/fixtures/test.mp4').metadata
63
- end
64
- end
65
-
66
- context 'when input is url' do
67
- should 'not be empty' do
68
- h = {:audio_codec=>"aac", :audio_sample_rate=>48000, :audio_bitrate_in_kbps=>83, :channels=>2, :duration_in_ms=>5570, :total_bitrate_in_kbps=>551, :video_codec=>"h264", :width=>560, :height=>320, :video_bitrate_in_kbps=>465, :frame_rate=>30.0, :file_size_in_bytes=>383631, :format=>"mp4"}
69
- assert_equal h, VideoConverter::Input.new(@test_url).metadata
70
- end
71
- end
72
- end
73
- end