video_converter 0.6.2 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,13 +4,13 @@ require "shellwords"
4
4
  require "video_converter/array"
5
5
  require "video_converter/base"
6
6
  require "video_converter/command"
7
- require "video_converter/ffmpeg"
8
7
  require "video_converter/faststart"
8
+ require "video_converter/ffmpeg"
9
9
  require "video_converter/hash"
10
10
  require "video_converter/hls"
11
- require "video_converter/hds"
12
11
  require "video_converter/input"
13
12
  require "video_converter/live_segmenter"
13
+ require "video_converter/mp4frag"
14
14
  require "video_converter/object"
15
15
  require "video_converter/output"
16
16
  require "video_converter/version"
@@ -2,40 +2,28 @@
2
2
 
3
3
  module VideoConverter
4
4
  class Base
5
- attr_accessor :uid, :outputs, :inputs, :clear_tmp
5
+ attr_accessor :inputs, :outputs
6
6
 
7
7
  def initialize params
8
- self.uid = params[:uid] || (Socket.gethostname + object_id.to_s)
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)
14
- end
15
- self.clear_tmp = params[:clear_tmp].nil? ? true : params[:clear_tmp]
8
+ self.inputs = Array.wrap(params[:input] || params[:inputs]).map { |input| Input.new(input) }
9
+ self.outputs = Array.wrap(params[:output] || params[:outputs]).map { |output| Output.new(output.merge(:uid => params[:uid] ? params[:uid].to_s : (Socket.gethostname + object_id.to_s))) }
16
10
  end
17
11
 
18
12
  def run
19
- success = convert && faststart && make_screenshots && segment
20
- clear if clear_tmp && success
21
- success
13
+ convert && faststart && make_screenshots && segment && clear
22
14
  end
23
15
 
16
+ # XXX inject instead of each would be better
24
17
  def convert
25
18
  success = true
26
- inputs.each do |input|
27
- input.output_groups.each do |group|
28
- success &&= Ffmpeg.new(input, group).run
29
- end
30
- end
19
+ inputs.each { |input| success &&= Ffmpeg.new(input, outputs).run }
31
20
  success
32
21
  end
33
22
 
23
+ # TODO use for faststart ffmpeg moveflags
34
24
  def faststart
35
25
  success = true
36
- outputs.each do |output|
37
- success &&= Faststart.new(output).run if output.faststart
38
- end
26
+ outputs.each { |output| success &&= Faststart.new(output).run if output.faststart }
39
27
  success
40
28
  end
41
29
 
@@ -50,35 +38,36 @@ module VideoConverter
50
38
  def segment
51
39
  success = true
52
40
  inputs.each do |input|
53
- input.output_groups.each do |group|
41
+ input.output_groups(input.select_outputs(outputs)).each do |group|
54
42
  if playlist = group.detect { |output| output.type == 'playlist' }
55
- success &&= if playlist.format == 'm3u8'
56
- LiveSegmenter.new(input, group).run
43
+ success &&= if File.extname(playlist.filename) == '.m3u8'
44
+ LiveSegmenter.run(input, group)
57
45
  else
58
- Hds.new(input, group).run
46
+ Mp4frag.run(input, group)
59
47
  end
60
48
  end
61
49
  end
62
50
  end
51
+ success
63
52
  end
64
53
 
65
54
  def split
66
- Ffmpeg.new(inputs.first, outputs).split
55
+ Ffmpeg.split(inputs.first, outputs.first)
67
56
  end
68
57
 
69
58
  def concat
70
- list = File.join(outputs.first.work_dir, 'list.txt')
71
- # NOTE ffmpeg concat list requires unescaped files
72
- File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.unescape)}'" }.join("\n"))
73
- success = Ffmpeg.new(list, outputs).concat
74
- FileUtils.rm list if success
75
- success
59
+ Ffmpeg.concat(inputs, outputs.first)
60
+ end
61
+
62
+ def mux
63
+ Ffmpeg.mux(inputs, outputs.first)
76
64
  end
77
65
 
78
66
  def clear
79
- `cat #{outputs.first.log} >> #{VideoConverter.log} && rm #{outputs.first.log}`
80
- outputs.map { |output| output.passlogfile }.uniq.compact.each { |passlogfile| `rm #{passlogfile}*` }
81
- outputs.select { |output| output.type == 'segmented' }.each { |output| `rm #{output.ffmpeg_output}` }
67
+ Command.new("cat #{outputs.first.log} >> #{VideoConverter.log} && rm #{outputs.first.log}").execute
68
+ outputs.map { |output| output.options[:passlogfile] }.uniq.compact.each { |passlogfile| Command.new("rm #{passlogfile}*").execute }
69
+ outputs.select { |output| output.type == 'segmented' }.each { |output| Command.new("rm #{output.ffmpeg_output}").execute }
70
+ true
82
71
  end
83
72
  end
84
73
  end
@@ -6,7 +6,7 @@ module VideoConverter
6
6
  attr_accessor :dry_run, :verbose
7
7
  end
8
8
  self.dry_run = false
9
- self.verbose = false
9
+ self.verbose = true
10
10
 
11
11
  def self.chain(*commands)
12
12
  commands.map { |c| "(#{c})" }.join(' && ')
@@ -3,120 +3,167 @@
3
3
  module VideoConverter
4
4
  class Ffmpeg
5
5
  class << self
6
- attr_accessor :bin, :ffprobe_bin, :options, :one_pass_command, :first_pass_command, :second_pass_command, :keyframes_command, :split_command, :concat_command
6
+ attr_accessor :aliases, :bin, :ffprobe_bin
7
+ attr_accessor :one_pass_command, :first_pass_command, :second_pass_command, :keyframes_command, :split_command, :concat_command, :mux_command
7
8
  end
8
9
 
10
+ self.aliases = {
11
+ :codec => 'c',
12
+ :video_codec => 'c:v',
13
+ :audio_codec => 'c:a',
14
+ :frame_rate => 'r',
15
+ :keyframe_interval => 'g',
16
+ :video_bitrate => 'b:v',
17
+ :audio_bitrate => 'b:a',
18
+ :size => 's',
19
+ :video_filter => 'vf',
20
+ :format => 'f',
21
+ :bitstream_format => 'bsf',
22
+ :pixel_format => 'pix_fmt'
23
+ }
9
24
  self.bin = '/usr/local/bin/ffmpeg'
10
25
  self.ffprobe_bin = '/usr/local/bin/ffprobe'
11
- self.options = {
12
- :codec => '-c',
13
- :video_codec => '-c:v',
14
- :audio_codec => '-c:a',
15
- :frame_rate => '-r',
16
- :keyint_min => '-keyint_min',
17
- :keyframe_interval => '-g',
18
- :force_keyframes => '-force_key_frames',
19
- :passlogfile => '-passlogfile',
20
- :video_bitrate => '-b:v',
21
- :audio_bitrate => '-b:a',
22
- :size => '-s',
23
- :video_filter => '-vf',
24
- :threads => '-threads',
25
- :format => '-f',
26
- :bitstream_format => '-bsf',
27
- :pixel_format => '-pix_fmt',
28
- :deinterlace => '-deinterlace',
29
- :map => '-map',
30
- :segment_time => '-segment_time',
31
- :reset_timestamps => '-reset_timestamps'
32
- }
26
+
33
27
  self.one_pass_command = '%{bin} -i %{input} -y %{options} %{output} 1>>%{log} 2>&1 || exit 1'
34
28
  self.first_pass_command = '%{bin} -i %{input} -y -pass 1 -an %{options} /dev/null 1>>%{log} 2>&1 || exit 1'
35
29
  self.second_pass_command = '%{bin} -i %{input} -y -pass 2 %{options} %{output} 1>>%{log} 2>&1 || exit 1'
36
30
  self.keyframes_command = '%{ffprobe_bin} -show_frames -select_streams v:0 -print_format csv %{input} | grep frame,video,1 | cut -d\',\' -f5 | tr "\n" "," | sed \'s/,$//\''
37
- self.split_command = '%{bin} -fflags +genpts -i %{input} -segment_time %{segment_time} -reset_timestamps 1 -c:v %{video_codec} -c:a %{audio_codec} -map 0:0 -map 0:1 -f segment %{output} 1>>%{log} 2>&1 || exit 1'
38
- self.concat_command = "%{bin} -f concat -i %{input} -c:v %{video_codec} -c:a %{audio_codec} %{output} 1>>%{log} 2>&1 || exit 1"
31
+ self.split_command = '%{bin} -fflags +genpts -i %{input} %{options} %{output} 1>>%{log} 2>&1 || exit 1'
32
+ self.concat_command = "%{bin} -f concat -i %{input} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
33
+ self.mux_command = "%{bin} %{inputs} %{maps} %{options} %{output} 1>>%{log} 2>&1 || exit 1"
34
+
35
+ def self.split(input, output)
36
+ output.options = { :format => 'segment', :map => 0, :codec => 'copy', :reset_timestamps => 1 }.merge(output.options)
37
+ Command.new(split_command, prepare_params(input, output)).execute
38
+ end
39
+
40
+ def self.concat(inputs, output, method = nil)
41
+ method = %w(ts mpg mpeg).include?(File.extname(inputs.first.to_s).delete('.')) ? :protocol : :muxer unless method
42
+ output.options = { :codec => 'copy' }.merge(output.options)
43
+ send("concat_#{method}", inputs, output)
44
+ end
45
+
46
+ def self.mux(inputs, output)
47
+ output.options = { :codec => 'copy' }.merge(output.options)
48
+ Command.new(mux_command, prepare_params(nil, output).merge({
49
+ :inputs => inputs.map { |i| "-i #{i}" }.join(' '),
50
+ :maps => inputs.each_with_index.map { |_,i| "-map #{i}:0" }.join(' ')
51
+ })).execute
52
+ end
53
+
54
+ attr_accessor :input, :outputs
39
55
 
40
- attr_accessor :input, :group
56
+ def initialize input, outputs
57
+ self.input = input
58
+ self.outputs = input.select_outputs(outputs)
41
59
 
42
- def initialize input, group
43
- self.input = input
44
- self.group = group
60
+ self.outputs.each do |output|
61
+ # autorotate
62
+ if output.type != 'playlist' && [nil, true].include?(output.rotate) && input.metadata[:rotate]
63
+ output.rotate = 360 - input.metadata[:rotate]
64
+ end
65
+ # autodeinterlace
66
+ output.options[:deinterlace] = input.metadata[:interlaced] if output.options[:deinterlace].nil?
67
+ # video filter
68
+ video_filter = [output.options[:video_filter]].compact
69
+ video_filter << "scale=#{output.width}:trunc\\(ow/a/2\\)*2" if output.width && !output.height
70
+ video_filter << "scale=trunc\\(oh*a/2\\)*2:#{output.height}" if output.height && !output.width
71
+ video_filter << { 90 => 'transpose=2', 180 => 'transpose=2,transpose=2', 270 => 'transpose=1' }[output.rotate] if output.rotate
72
+ output.options[:video_filter] = video_filter.join(',') if video_filter.any?
73
+
74
+ output.options[:format] ||= File.extname(output.filename).delete('.')
75
+ output.options = {
76
+ :keyint_min => 25,
77
+ :keyframe_interval => 100,
78
+ :threads => 1,
79
+ :video_codec => 'libx264',
80
+ :audio_codec => 'libfaac',
81
+ :pixel_format => 'yuv420p'
82
+ }.merge(output.options) unless output.type == 'playlist'
83
+ end
45
84
  end
46
85
 
47
86
  def run
48
87
  success = true
49
88
  threads = []
50
- common_first_pass = false
51
-
52
- qualities = group.select { |output| output.type != 'playlist' }
53
- # if all qualities in group contain one_pass, use one_pass_command for ffmpeg
54
- unless one_pass = qualities.inject { |r, q| r && q.one_pass }
55
- # if group qualities have different sizes use force_keyframes and separate first passes
56
- if common_first_pass = qualities.map { |quality| quality.height }.uniq.count == 1
57
- # first pass is executed with the best group quality, defined by bitrate or size
58
- best_quality = qualities.sort do |q1, q2|
59
- res = q1.video_bitrate.to_i <=> q2.video_bitrate.to_i
89
+
90
+ input.output_groups(outputs).each_with_index do |group, group_index|
91
+ qualities = group.select { |output| output.type != 'playlist' }
92
+
93
+ # common first pass
94
+ if !one_pass?(qualities) && common_first_pass?(qualities)
95
+ qualities.each { |output| output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}.log") }
96
+ best_quality = qualities.sort do |q1, q2|
97
+ res = q1.options[:video_bitrate].to_i <=> q2.options[:video_bitrate].to_i
60
98
  res = q1.height.to_i <=> q2.height.to_i if res == 0
61
99
  res = q1.width.to_i <=> q2.width.to_i if res == 0
100
+ # TODO compare by size
62
101
  res
63
102
  end.last
64
- success &&= Command.new(self.class.first_pass_command, prepare_params(input, best_quality)).execute
65
- end
66
- end
67
- qualities.each do |output|
68
- command = if one_pass
69
- self.class.one_pass_command
70
- elsif !common_first_pass
71
- Command.chain(self.class.first_pass_command, self.class.second_pass_command)
72
- else
73
- self.class.second_pass_command
103
+ success &&= Command.new(self.class.first_pass_command, self.class.prepare_params(input, best_quality)).execute
74
104
  end
75
- command = Command.new(command, prepare_params(input, output))
76
- if VideoConverter.paral
77
- threads << Thread.new { success &&= command.execute }
78
- else
79
- success &&= command.execute
105
+
106
+ qualities.each_with_index do |output, output_index|
107
+ command = if one_pass?(qualities)
108
+ self.class.one_pass_command
109
+ elsif common_first_pass?(qualities)
110
+ self.class.second_pass_command
111
+ else
112
+ output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
113
+ output.options[:force_key_frames] = (input.metadata[:duration_in_ms] / 1000 / Output.keyframe_interval_in_seconds).times.to_a.map { |t| t * Output.keyframe_interval_in_seconds }.join(',')
114
+ Command.chain(self.class.first_pass_command, self.class.second_pass_command)
115
+ end
116
+
117
+ # run ffmpeg
118
+ command = Command.new(command, self.class.prepare_params(input, output))
119
+ if VideoConverter.paral
120
+ threads << Thread.new { success &&= command.execute }
121
+ else
122
+ success &&= command.execute
123
+ end
80
124
  end
81
125
  end
82
126
  threads.each { |t| t.join } if VideoConverter.paral
83
127
  success
84
128
  end
85
129
 
86
- def split
87
- Command.new(self.class.split_command, prepare_params(input, group.first)).execute
130
+ private
131
+
132
+ def self.concat_muxer(inputs, output)
133
+ list = File.join(output.work_dir, 'list.txt')
134
+ # NOTE ffmpeg concat list requires unescaped files
135
+ File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.unescape)}'" }.join("\n"))
136
+ success = Command.new(concat_command, prepare_params(list, output)).execute
137
+ FileUtils.rm list if success
138
+ success
88
139
  end
89
140
 
90
- def concat
91
- Command.new(self.class.concat_command, prepare_params(input, group.first)).execute
141
+ def self.concat_protocol(inputs, output)
142
+ Command.new(one_pass_command, prepare_params('"concat:' + inputs.join('|') + '"', output)).execute
92
143
  end
93
144
 
94
- private
145
+ def common_first_pass?(qualities)
146
+ # if group qualities have different sizes use force_key_frames and separate first passes
147
+ qualities.uniq { |o| o.height }.count == 1 && qualities.uniq { |o| o.width }.count == 1 && qualities.uniq { |o| o.options[:size] }.count == 1
148
+ end
95
149
 
96
- def prepare_params input, output
97
- output.video_filter = []
98
- output.video_filter << "scale=#{output.width}:trunc\\(ow/a/2\\)*2" if output.width && !output.height
99
- output.video_filter << "scale=trunc\\(oh*a/2\\)*2:#{output.height}" if output.height && !output.width
100
- output.video_filter << { 90 => 'transpose=2', 180 => 'transpose=2,transpose=2', 270 => 'transpose=1' }[output.rotate] if output.rotate
101
- output.video_filter = output.video_filter.join(',')
150
+ def one_pass?(qualities)
151
+ qualities.inject(true) { |r, q| r && q.one_pass }
152
+ end
102
153
 
154
+ def self.prepare_params input, output
155
+
103
156
  {
104
- :bin => self.class.bin,
157
+ :bin => bin,
105
158
  :input => input.to_s,
106
- :log => output.log,
107
- :output => output.ffmpeg_output,
108
- :video_codec => output.video_codec,
109
- :audio_codec => output.audio_codec,
110
- :segment_time => output.segment_time,
111
- :options => self.class.options.map do |output_option, ffmpeg_option|
112
- if output.send(output_option).present?
113
- if output.send(output_option) == true
114
- ffmpeg_option
115
- else
116
- ffmpeg_option + ' ' + output.send(output_option).to_s
117
- end
159
+ :options => output.options.map do |option, value|
160
+ if value && !output.respond_to?(option)
161
+ option = '-' + (aliases[option] || option).to_s
162
+ value == true ? option : "#{option} #{value}"
118
163
  end
119
- end.join(' ')
164
+ end.compact.join(' '),
165
+ :output => output.ffmpeg_output,
166
+ :log => output.log
120
167
  }
121
168
  end
122
169
  end
@@ -3,57 +3,18 @@
3
3
  module VideoConverter
4
4
  class Input
5
5
  class << self
6
- attr_accessor :metadata_command, :show_frame_command
6
+ attr_accessor :metadata_command, :show_streams_command, :show_frames_command
7
7
  end
8
8
 
9
9
  self.metadata_command = "%{ffprobe_bin} %{input} 2>&1"
10
- self.show_frame_command = "%{ffprobe_bin} -show_frames -select_streams v %{input} 2>/dev/null | head -n 24"
10
+ self.show_streams_command = "%{ffprobe_bin} -show_streams -select_streams %{stream} %{input} 2>/dev/null"
11
+ self.show_frames_command = "%{ffprobe_bin} -show_frames -select_streams %{stream} %{input} 2>/dev/null | head -n 24"
11
12
 
12
13
  attr_accessor :input, :output_groups, :metadata
13
14
 
14
15
  def initialize input, outputs = []
15
- raise ArgumentError.new('input is needed') if input.blank?
16
- self.input = input
16
+ self.input = input or raise ArgumentError.new('Input requred')
17
17
  raise ArgumentError.new("#{input} does not exist") unless exists?
18
- self.metadata = get_metadata
19
-
20
- # for many inputs case take outputs for this input
21
- outputs = outputs.select { |output| !output.path || output.path == input.to_s }
22
- self.output_groups = []
23
-
24
- # qualities with the same playlist is a one group
25
- outputs.select { |output| output.type == 'playlist' }.each_with_index do |playlist, group_index|
26
- paths = playlist.streams.map { |stream| stream[:path] }
27
- output_group = outputs.select { |output| paths.include?(output.filename) }
28
- if output_group.any?
29
- # if group qualities have different sizes use force_keyframes and separate first passes
30
- common_first_pass = output_group.map { |output| output.height }.uniq.count == 1
31
- output_group.each_with_index do |output, output_index|
32
- unless output.one_pass
33
- if common_first_pass
34
- output.passlogfile = File.join(output.work_dir, "group#{group_index}.log")
35
- else
36
- output.passlogfile = File.join(output.work_dir, "group#{group_index}_#{output_index}.log")
37
- output.force_keyframes = (metadata[:duration_in_ms] / 1000 / Output.keyframe_interval_in_seconds).times.to_a.map { |t| t * Output.keyframe_interval_in_seconds }.join(',')
38
- output.frame_rate = output.keyint_min = output.keyframe_interval = nil
39
- end
40
- end
41
- end
42
- self.output_groups << output_group.unshift(playlist)
43
- end
44
- end
45
- # qualities without playlist are separate groups
46
- (outputs - output_groups.flatten).each { |output| self.output_groups << [output] }
47
-
48
- # set output parameters depending of input
49
- outputs.each do |output|
50
- # autorotate
51
- if output.type != 'playlist' && [nil, true].include?(output.rotate) && metadata[:rotate]
52
- output.rotate = 360 - metadata[:rotate]
53
- end
54
- # autodeinterlace
55
- output.deinterlace = metadata[:interlaced] if output.deinterlace.nil?
56
- end
57
18
  end
58
19
 
59
20
  def to_s
@@ -64,6 +25,70 @@ module VideoConverter
64
25
  input.gsub(/\\+([^n])/, '\1')
65
26
  end
66
27
 
28
+ def metadata
29
+ unless @metadata
30
+ @metadata = {}
31
+ # common metadata
32
+ s = Command.new(self.class.metadata_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
33
+ if m = s.match(/Stream\s#(\d:\d).*?Audio:\s*(\w+).*?(\d+)\s*Hz.*?(\d+)\s*kb\/s.*?$/)
34
+ @metadata[:audio_stream] = m[1]
35
+ @metadata[:audio_codec] = m[2]
36
+ @metadata[:audio_sample_rate] = m[3].to_i
37
+ @metadata[:audio_bitrate_in_kbps] = m[4].to_i
38
+ end
39
+ @metadata[:channels] = s.scan(/Stream #\d+:\d+/).count
40
+ if m = s.match(/Duration:\s+(\d+):(\d+):([\d.]+).*?bitrate:\s*(\d+)\s*kb\/s/)
41
+ @metadata[:duration_in_ms] = ((m[1].to_i * 3600 + m[2].to_i * 60 + m[3].to_f) * 1000).to_i
42
+ @metadata[:total_bitrate_in_kbps] = m[4].to_i
43
+ end
44
+ if m = s.match(/Stream\s#(\d:\d).*?Video:\s(\S+).*?,\s*((\d+)x(\d+))(?:.*?(\d+)\s*kb\/s.*?([\d.]+)\s*fps)?/)
45
+ @metadata[:video_stream] = m[1]
46
+ @metadata[:video_codec] = m[2]
47
+ @metadata[:width] = m[4].to_i
48
+ @metadata[:height] = m[5].to_i
49
+ @metadata[:video_bitrate_in_kbps] = m[6].to_i
50
+ @metadata[:frame_rate] = m[7].to_f
51
+ end
52
+ if m = s.match(/rotate\s*\:\s*(\d+)/)
53
+ @metadata[:rotate] = m[1].to_i
54
+ end
55
+ if is_http?
56
+ url = URI.parse(input)
57
+ Net::HTTP.start(url.host) do |http|
58
+ response = http.request_head url.path
59
+ @metadata[:file_size_in_bytes] = response['content-length'].to_i
60
+ end
61
+ elsif is_local?
62
+ @metadata[:file_size_in_bytes] = File.size(input.gsub('\\', ''))
63
+ end
64
+ @metadata[:format] = File.extname(input).delete('.')
65
+
66
+ # stream metadata
67
+ s = Command.new(self.class.show_streams_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :stream => 'v', :input => input).capture
68
+ @metadata[:video_start_time] = s.match(/start_time=([\d.]+)/).to_a[1].to_f
69
+
70
+ # frame metadata
71
+ s = Command.new(self.class.show_frames_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :stream => 'v', :input => input).capture
72
+ @metadata[:interlaced] = true if s.include?('interlaced_frame=1')
73
+ end
74
+ @metadata
75
+ end
76
+
77
+ def select_outputs(outputs)
78
+ outputs.select { |output| !output.path || output.path == input }
79
+ end
80
+
81
+ def output_groups(outputs)
82
+ groups = []
83
+ outputs.select { |output| output.type == 'playlist' }.each do |playlist|
84
+ paths = playlist.streams.map { |stream| stream[:path] }
85
+ groups << outputs.select { |output| paths.include?(output.filename) }.unshift(playlist)
86
+ end
87
+ # qualities without playlist are separate groups
88
+ (outputs - groups.flatten).each { |output| groups << [output] }
89
+ groups
90
+ end
91
+
67
92
  private
68
93
 
69
94
  def exists?
@@ -87,46 +112,5 @@ module VideoConverter
87
112
  File.file?(input.gsub('\\', ''))
88
113
  end
89
114
 
90
- def get_metadata
91
- metadata = {}
92
- # common metadata
93
- s = Command.new(self.class.metadata_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
94
- if m = s.match(/Stream.*?Audio:\s*(\w+).*?(\d+)\s*Hz.*?(\d+)\s*kb\/s.*?$/)
95
- metadata[:audio_codec] = m[1]
96
- metadata[:audio_sample_rate] = m[2].to_i
97
- metadata[:audio_bitrate_in_kbps] = m[3].to_i
98
- end
99
- metadata[:channels] = s.scan(/Stream #\d+:\d+/).count
100
- if m = s.match(/Duration:\s+(\d+):(\d+):([\d.]+).*?bitrate:\s*(\d+)\s*kb\/s/)
101
- metadata[:duration_in_ms] = ((m[1].to_i * 3600 + m[2].to_i * 60 + m[3].to_f) * 1000).to_i
102
- metadata[:total_bitrate_in_kbps] = m[4].to_i
103
- end
104
- if m = s.match(/Stream.*?Video:\s(\S+).*?,\s*((\d+)x(\d+)).*?(\d+)\s*kb\/s.*?([\d.]+)\s*fps/)
105
- metadata[:video_codec] = m[1]
106
- metadata[:width] = m[3].to_i
107
- metadata[:height] = m[4].to_i
108
- metadata[:video_bitrate_in_kbps] = m[5].to_i
109
- metadata[:frame_rate] = m[6].to_f
110
- end
111
- if m = s.match(/rotate\s*\:\s*(\d+)/)
112
- metadata[:rotate] = m[1].to_i
113
- end
114
- if is_http?
115
- url = URI.parse(input)
116
- Net::HTTP.start(url.host) do |http|
117
- response = http.request_head url.path
118
- metadata[:file_size_in_bytes] = response['content-length'].to_i
119
- end
120
- elsif is_local?
121
- metadata[:file_size_in_bytes] = File.size(input.gsub('\\', ''))
122
- end
123
- metadata[:format] = File.extname(input).delete('.')
124
-
125
- # frame metadata
126
- s = Command.new(self.class.show_frame_command, :ffprobe_bin => Ffmpeg.ffprobe_bin, :input => input).capture
127
- metadata[:interlaced] = true if s.include?('interlaced_frame=1')
128
-
129
- metadata
130
- end
131
115
  end
132
116
  end
@@ -13,63 +13,56 @@ module VideoConverter
13
13
 
14
14
  self.command = '%{ffmpeg_bin} -i %{ffmpeg_output} -c:v copy -c:a copy -f mpegts pipe:1 2>>/dev/null | %{bin} %{keyframe_interval_in_seconds} %{chunks_dir} %{chunk_prefix} %{encoding_profile} 1>>%{log} 2>&1'
15
15
 
16
- attr_accessor :input, :group
17
-
18
- def initialize input, group
19
- self.input = input
20
- self.group = group
21
- end
22
-
23
- def run
16
+ def self.run(input, outputs)
24
17
  success = true
25
18
  threads = []
26
19
  p = Proc.new do |output|
27
20
  make_chunks(output) && gen_quality_playlist(output)
28
21
  end
29
- group.select { |output| output.type != 'playlist' }.each do |output|
22
+ outputs.select { |output| output.type != 'playlist' }.each do |output|
30
23
  if VideoConverter.paral
31
24
  threads << Thread.new { success &&= p.call(output) }
32
25
  else
33
26
  success &&= p.call(output)
34
27
  end
35
28
  end
36
- success &&= gen_group_playlist(group.detect { |output| output.type == 'playlist' })
29
+ success &&= gen_group_playlist(outputs.detect { |output| output.type == 'playlist' })
37
30
  threads.each { |t| t.join } if VideoConverter.paral
38
31
  success
39
32
  end
40
33
 
41
34
  private
42
35
 
43
- def make_chunks output
44
- Command.new(self.class.command, prepare_params(output)).execute
36
+ def self.make_chunks output
37
+ Command.new(command, prepare_params(output)).execute
45
38
  end
46
39
 
47
- def gen_quality_playlist output
40
+ def self.gen_quality_playlist output
48
41
  res = ''
49
42
  durations = []
50
43
  # order desc
51
- chunks = Dir::glob(File.join(output.chunks_dir, "#{self.class.chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
44
+ chunks = Dir::glob(File.join(output.chunks_dir, "#{chunk_prefix}-*[0-9].ts")).sort do |c1, c2|
52
45
  File.basename(c2).match(/\d+/).to_s.to_i <=> File.basename(c1).match(/\d+/).to_s.to_i
53
46
  end
54
47
  # chunk duration = (pts of first frame of the next chunk - pts of first frame of current chunk) / time_base
55
48
  # for the last chunks the last two pts are used
56
- 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 }
49
+ prl_pts, l_pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunks.first} | tail -n2 2>&1`.split("\n").map { |l| l.split(',')[3].to_i }
57
50
  # NOTE for case when chunk has one frame
58
51
  l_pts ||= prl_pts
59
52
  next_chunk_pts = 2 * l_pts - prl_pts
60
- 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
53
+ time_base = `#{Ffmpeg.ffprobe_bin} -show_streams -select_streams #{select_streams} -loglevel fatal #{chunks.first} 2>&1`.match(/\ntime_base=1\/(\d+)/)[1].to_f
61
54
  chunks.each do |chunk|
62
- 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
55
+ pts = `#{Ffmpeg.ffprobe_bin} -show_frames -select_streams #{select_streams} -print_format csv -loglevel fatal #{chunk} | head -n1 2>&1`.split(',')[3].to_i
63
56
  durations << (duration = (next_chunk_pts - pts) / time_base)
64
57
  next_chunk_pts = pts
65
58
  res = File.join(File.basename(output.chunks_dir), File.basename(chunk)) + "\n" + res
66
59
  res = "#EXTINF:%0.2f,\n" % duration + res
67
60
  end
68
- res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
61
+ res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:#{durations.max.to_f.ceil}\n#EXT-X-MEDIA-SEQUENCE:0\n" + res + "#EXT-X-ENDLIST"
69
62
  !!File.open(File.join(output.work_dir, output.filename), 'w') { |f| f.write res }
70
63
  end
71
64
 
72
- def gen_group_playlist playlist
65
+ def self.gen_group_playlist playlist
73
66
  res = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n"
74
67
  playlist.streams.sort { |s1, s2| s1[:bandwidth].to_i <=> s2[:bandwidth].to_i }.each do |stream|
75
68
  res += "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=#{stream[:bandwidth].to_i * 1000}\n"
@@ -80,17 +73,17 @@ module VideoConverter
80
73
  true
81
74
  end
82
75
 
83
- def prepare_params output
76
+ def self.prepare_params output
84
77
  {
85
78
  :ffmpeg_bin => Ffmpeg.bin,
86
79
  :ffmpeg_output => output.ffmpeg_output,
87
- :bin => self.class.bin,
80
+ :bin => bin,
88
81
  :keyframe_interval_in_seconds => Output.keyframe_interval_in_seconds,
89
82
  :chunks_dir => output.chunks_dir,
90
- :chunk_prefix => self.class.chunk_prefix,
91
- :encoding_profile => self.class.encoding_profile,
83
+ :chunk_prefix => chunk_prefix,
84
+ :encoding_profile => encoding_profile,
92
85
  :log => output.log
93
86
  }
94
87
  end
95
88
  end
96
- end
89
+ end
@@ -1,24 +1,17 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module VideoConverter
4
- class Hds
4
+ class Mp4frag
5
5
  class << self
6
6
  attr_accessor :bin, :command
7
7
  end
8
8
  self.bin = '/usr/local/bin/mp4frag'
9
9
  self.command = '%{bin} %{inputs} --manifest %{manifest} --index 1>>%{log} 2>&1 || exit 1'
10
10
 
11
- attr_accessor :input, :group
12
-
13
- def initialize input, group
14
- self.input = input
15
- self.group = group
16
- end
17
-
18
- def run
11
+ def self.run(input, outputs)
19
12
  success = true
20
13
  threads = []
21
- command = Command.new(self.class.command, prepare_params(group))
14
+ command = Command.new(self.command, prepare_params(outputs))
22
15
  if VideoConverter.paral
23
16
  threads << Thread.new { success &&= command.execute }
24
17
  else
@@ -30,12 +23,12 @@ module VideoConverter
30
23
 
31
24
  private
32
25
 
33
- def prepare_params(group)
26
+ def self.prepare_params(outputs)
34
27
  {
35
- :bin => self.class.bin,
36
- :inputs => group.select { |output| output.type != 'playlist' }.map { |input| "--src #{input.ffmpeg_output}" }.join(' '),
37
- :manifest => group.detect { |output| output.type == 'playlist' }.ffmpeg_output,
38
- :log => group.first.log
28
+ :bin => bin,
29
+ :inputs => outputs.select { |output| output.type != 'playlist' }.map { |input| "--src #{input.ffmpeg_output}" }.join(' '),
30
+ :manifest => outputs.detect { |output| output.type == 'playlist' }.ffmpeg_output,
31
+ :log => outputs.first.log
39
32
  }
40
33
  end
41
34
  end
@@ -3,98 +3,34 @@
3
3
  module VideoConverter
4
4
  class Output
5
5
  class << self
6
- attr_accessor :work_dir, :keyint_min, :keyframe_interval, :keyframe_interval_in_seconds, :threads, :video_codec, :audio_codec, :pixel_format
6
+ attr_accessor :work_dir, :log, :keyframe_interval_in_seconds
7
7
  end
8
-
9
8
  self.work_dir = '/tmp'
10
- self.keyint_min = 25
11
- self.keyframe_interval = 100
9
+ self.log = 'converter.log'
12
10
  self.keyframe_interval_in_seconds = 4
13
- self.threads = 1
14
- self.video_codec = 'libx264'
15
- self.audio_codec = 'libfaac'
16
- self.pixel_format = 'yuv420p'
17
11
 
18
- attr_accessor :uid, :work_dir, :log, :threads, :passlogfile
19
- attr_accessor :type, :filename
20
- attr_accessor :format, :ffmpeg_output, :codec, :video_codec, :audio_codec, :bitstream_format, :pixel_format, :map
21
- attr_accessor :one_pass, :video_bitrate, :audio_bitrate
22
- attr_accessor :streams, :path, :chunks_dir, :segment_time, :reset_timestamps
23
- attr_accessor :frame_rate, :keyint_min, :keyframe_interval, :force_keyframes
24
- attr_accessor :size, :width, :height, :video_filter
25
- attr_accessor :thumbnails
26
- attr_accessor :rotate, :deinterlace
27
- attr_accessor :faststart
12
+ attr_accessor :work_dir, :filename, :log, :type, :chunks_dir, :ffmpeg_output, :path, :streams, :width, :height, :one_pass, :rotate, :faststart, :thumbnails, :uid, :options
28
13
 
29
14
  def initialize params = {}
30
- # Inner
31
- self.uid = params[:uid].to_s
32
- self.work_dir = File.join(self.class.work_dir, uid)
15
+ self.work_dir = File.join(self.class.work_dir, params[:uid])
33
16
  FileUtils.mkdir_p(work_dir)
34
- self.log = File.join(work_dir, 'converter.log')
35
- self.threads = params[:threads] || self.class.threads
36
- # NOTE passlogile is defined in input
37
-
38
- # General output options
39
- self.type = params[:type]
40
- raise ArgumentError.new('Incorrect type') if type && !%w(default segmented playlist).include?(type)
41
- self.filename = params[:filename] or raise ArgumentError.new('filename required')
42
-
43
- # Formats and codecs
44
- if type == 'segmented'
45
- self.format = 'mpegts'
46
- self.ffmpeg_output = File.join(work_dir, File.basename(filename, '.*') + '.ts')
47
- else
48
- self.format = params[:format] || File.extname(filename).sub('.', '')
49
- self.ffmpeg_output = File.join(work_dir, filename)
50
- raise ArgumentError.new('Invalid playlist extension') if type == 'playlist' && !['f4m', 'm3u8'].include?(format)
51
- end
52
- self.codec = params[:codec]
53
- self.video_codec = params[:video_codec] || self.class.video_codec unless codec
54
- self.audio_codec = params[:audio_codec] || self.class.audio_codec unless codec
55
- self.bitstream_format = params[:bitstream_format]
56
- self.pixel_format = params[:pixel_format] || self.class.pixel_format
57
- self.map = params[:map]
58
-
59
- # Rate controle
60
- self.one_pass = !!params[:one_pass]
61
- self.video_bitrate = "#{params[:video_bitrate]}k" if params[:video_bitrate]
62
- self.audio_bitrate = "#{params[:audio_bitrate]}k" if params[:audio_bitrate]
63
-
64
- # Segmented streaming
65
- self.streams = params[:streams]
66
- self.path = params[:path]
17
+ self.filename = params[:filename] or raise ArgumentError.new('Filename required')
18
+ self.log = File.join(work_dir, self.class.log)
19
+ self.type = params[:type] || 'default'
67
20
  if type == 'segmented'
68
21
  self.chunks_dir = File.join(work_dir, File.basename(filename, '.*'))
69
22
  FileUtils.mkdir_p(chunks_dir)
23
+ self.ffmpeg_output = chunks_dir + '.ts'
24
+ params[:format] = 'mpegts'
25
+ else
26
+ self.ffmpeg_output = File.join(work_dir, filename)
70
27
  end
71
- self.segment_time = params[:segment_time]
72
- self.reset_timestamps = params[:reset_timestamps]
73
-
74
- # Frame rate
75
- self.frame_rate = params[:frame_rate]
76
- self.keyint_min = params[:keyint_min] || self.class.keyint_min
77
- self.keyframe_interval = params[:keyframe_interval] || self.class.keyframe_interval
78
-
79
- # Resolution
80
- self.size = params[:size]
81
- self.width = params[:size] ? params[:size].split('x').first : params[:width]
82
- self.height = params[:size] ? params[:size].split('x').last : params[:height]
83
- self.size = "#{width}x#{height}" if !size && width && height
84
-
85
- #Thumbnails
86
- self.thumbnails = params[:thumbnails]
87
-
88
- # Video processing
89
- self.rotate = params[:rotate]
90
- unless [nil, true, false].include? rotate
91
- self.rotate = rotate.to_i
92
- raise ArgumentError.new('Invalid rotate') unless [0, 90, 180, 270].include? rotate
93
- end
94
- self.deinterlace = params[:deinterlace]
28
+ raise ArgumentError.new('Invalid type') unless %w(default segmented playlist).include?(type)
29
+ [:path, :streams, :width, :height, :one_pass, :rotate, :faststart, :thumbnails].each { |attr| self.send("#{attr}=", params[attr]) }
30
+ [:video_bitrate, :audio_bitrate].each { |bitrate| params[bitrate] = "#{params[bitrate]}k" if params[bitrate].is_a?(Numeric) }
95
31
 
96
- #Faststart
97
- self.faststart = params.has_key?(:faststart) ? params[:faststart] : self.format == 'mp4'
32
+ # options will be substituted to convertation commands
33
+ self.options = params
98
34
  end
99
35
  end
100
36
  end
@@ -1,3 +1,3 @@
1
1
  module VideoConverter
2
- VERSION = "0.6.2"
2
+ VERSION = "0.6.4"
3
3
  end
@@ -18,11 +18,11 @@ class VideoConverterTest < Test::Unit::TestCase
18
18
 
19
19
  should 'create qualities and thumbnails' do
20
20
  3.times.each do |n|
21
- q = File.join(VideoConverter::Output.work_dir, @c.uid, "q#{n + 1}.mp4")
21
+ q = File.join(@c.outputs.first.work_dir, "q#{n + 1}.mp4")
22
22
  assert File.exists?(q)
23
23
  assert File.size(q) > 0
24
24
  end
25
- assert_equal %w(. .. scr004.jpg scr004_norm.jpg scr005.jpg scr005_norm.jpg).sort, Dir.entries(File.join(VideoConverter::Output.work_dir, @c.uid, 'thumbnails')).sort
25
+ assert_equal %w(. .. scr004.jpg scr004_norm.jpg scr005.jpg scr005_norm.jpg).sort, Dir.entries(File.join(@c.outputs.first.work_dir, 'thumbnails')).sort
26
26
  end
27
27
  end
28
28
  end
@@ -52,10 +52,10 @@ class VideoConverterTest < Test::Unit::TestCase
52
52
  should 'generate hls' do
53
53
  %w(sd1 sd2 hd1 hd2).each do |quality|
54
54
  # should create chunks
55
- 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
55
+ assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
56
56
  # TODO verify that chunks have different quality (weight)
57
57
  # should create playlists
58
- assert File.exists?(playlist = File.join(VideoConverter::Output.work_dir, @c.uid, "#{quality}.m3u8"))
58
+ assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
59
59
  # TODO verify that playlist is valid (contain all chunks and modifiers)
60
60
  end
61
61
  end
@@ -83,12 +83,12 @@ class VideoConverterTest < Test::Unit::TestCase
83
83
 
84
84
  should 'generate hds with sync keyframes' do
85
85
  %w(sd1.mp4 sd2.mp4 playlist.f4m hd1.mp4 hd2.mp4 hd_playlist.f4m).each do |filename|
86
- assert File.exists?(File.join(VideoConverter::Output.work_dir, @c.uid, "#{filename}"))
86
+ assert File.exists?(File.join(@c.outputs.first.work_dir, "#{filename}"))
87
87
  end
88
88
 
89
89
  assert_equal(
90
- (k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(VideoConverter::Output.work_dir, @c.uid, 'sd1.mp4')).capture),
91
- (k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(VideoConverter::Output.work_dir, @c.uid, 'sd2.mp4')).capture)
90
+ (k1 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(@c.outputs.first.work_dir, 'sd1.mp4')).capture),
91
+ (k2 = VideoConverter::Command.new(VideoConverter::Ffmpeg.keyframes_command, :ffprobe_bin => VideoConverter::Ffmpeg.ffprobe_bin, :input => File.join(@c.outputs.first.work_dir, 'sd2.mp4')).capture)
92
92
  )
93
93
  end
94
94
  end
@@ -114,10 +114,10 @@ class VideoConverterTest < Test::Unit::TestCase
114
114
  should 'generate hls' do
115
115
  %w(q1 q2).each do |quality|
116
116
  # should create chunks
117
- 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
117
+ assert_equal ['s-00001.ts', 's-00002.ts', 's-00003.ts'], Dir.entries(File.join(@c.outputs.first.work_dir, quality)).delete_if { |e| ['.', '..'].include?(e) }.sort
118
118
  # TODO verify that chunks have the same quality (weight)
119
119
  # should create playlists
120
- assert File.exists?(playlist = File.join(VideoConverter::Output.work_dir, @c.uid, "#{quality}.m3u8"))
120
+ assert File.exists?(playlist = File.join(@c.outputs.first.work_dir, "#{quality}.m3u8"))
121
121
  # TODO verify that playlist is valid (contain all chunks and modifiers)
122
122
  end
123
123
  end
@@ -128,7 +128,7 @@ class VideoConverterTest < Test::Unit::TestCase
128
128
  setup do
129
129
  (@c = VideoConverter.new(
130
130
  :input => "test/fixtures/test (1).mp4",
131
- :output => { :segment_time => 2, :filename => '%01d.nut' }
131
+ :output => { :segment_time => 2, :codec => 'copy', :filename => '%01d.nut' }
132
132
  )).split
133
133
  end
134
134
  should 'segment file' do
@@ -153,4 +153,22 @@ class VideoConverterTest < Test::Unit::TestCase
153
153
  )
154
154
  end
155
155
  end
156
+
157
+ # context 'muxing' do
158
+ # setup do
159
+ # input = "test/fixtures/test (1).mp4"
160
+ # (c1 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:0', :filename => 'video.mp4' })).convert
161
+ # (c2 = VideoConverter.new(:uid => 'test', :input => input, :output => { :map => '0:1', :filename => 'audio.wav' })).convert
162
+ # (@c = VideoConverter.new(:uid => 'test', :input => [c1.outputs.first.ffmpeg_output, c2.outputs.first.ffmpeg_output], :output => { :filename => 'mux.mp4' })).mux
163
+ # @metadata = VideoConverter.new(:input => @c.outputs.first.ffmpeg_output).inputs.first.metadata
164
+ # end
165
+ # should 'mux streams' do
166
+ # assert File.exists?(@c.outputs.first.ffmpeg_output)
167
+ # assert_equal '0:0', @metadata[:video_stream]
168
+ # assert_equal '0:1', @metadata[:audio_stream]
169
+ # end
170
+ # teardown do
171
+ # #FileUtils.rm_r @c.outputs.first.work_dir
172
+ # end
173
+ # end
156
174
  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.6.2
4
+ version: 0.6.4
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-06-06 00:00:00.000000000 Z
12
+ date: 2014-06-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: video_screenshoter
@@ -112,10 +112,10 @@ files:
112
112
  - lib/video_converter/faststart.rb
113
113
  - lib/video_converter/ffmpeg.rb
114
114
  - lib/video_converter/hash.rb
115
- - lib/video_converter/hds.rb
116
115
  - lib/video_converter/hls.rb
117
116
  - lib/video_converter/input.rb
118
117
  - lib/video_converter/live_segmenter.rb
118
+ - lib/video_converter/mp4frag.rb
119
119
  - lib/video_converter/object.rb
120
120
  - lib/video_converter/output.rb
121
121
  - lib/video_converter/version.rb
@@ -142,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
142
  version: '0'
143
143
  segments:
144
144
  - 0
145
- hash: -1555004109844257794
145
+ hash: -4263200724433938900
146
146
  required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  none: false
148
148
  requirements:
@@ -151,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
151
  version: '0'
152
152
  segments:
153
153
  - 0
154
- hash: -1555004109844257794
154
+ hash: -4263200724433938900
155
155
  requirements:
156
156
  - ffmpeg, version 1.2 or greated configured with libx264 and libfaac
157
157
  - live_segmenter to convert to hls