video_converter 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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