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.
- data/lib/video_converter.rb +15 -31
- data/lib/video_converter/array.rb +11 -0
- data/lib/video_converter/base.rb +15 -39
- data/lib/video_converter/command.rb +4 -19
- data/lib/video_converter/ffmpeg.rb +60 -73
- data/lib/video_converter/hash.rb +32 -0
- data/lib/video_converter/input.rb +20 -18
- data/lib/video_converter/live_segmenter.rb +38 -30
- data/lib/video_converter/object.rb +9 -0
- data/lib/video_converter/output.rb +39 -60
- data/lib/video_converter/version.rb +1 -1
- data/test/fixtures/chunk0.ts +0 -0
- data/test/fixtures/chunk1.ts +0 -0
- data/test/fixtures/test (1).mp4 +0 -0
- data/test/fixtures/test_playlist.m3u8 +8 -0
- data/test/hls_test.rb +14 -0
- data/test/video_converter_test.rb +51 -119
- metadata +17 -11
- data/lib/video_converter/input_array.rb +0 -24
- data/lib/video_converter/output_array.rb +0 -21
- data/lib/video_converter/process.rb +0 -82
- data/test/fixtures/test.mp4 +0 -0
- data/test/input_test.rb +0 -73
data/lib/video_converter.rb
CHANGED
@@ -1,45 +1,29 @@
|
|
1
|
-
require "
|
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/
|
8
|
+
require "video_converter/hash"
|
9
|
+
require "video_converter/hls"
|
7
10
|
require "video_converter/input"
|
8
|
-
require "video_converter/
|
11
|
+
require "video_converter/live_segmenter"
|
12
|
+
require "video_converter/object"
|
9
13
|
require "video_converter/output"
|
10
|
-
require "video_converter/
|
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
|
data/lib/video_converter/base.rb
CHANGED
@@ -2,68 +2,44 @@
|
|
2
2
|
|
3
3
|
module VideoConverter
|
4
4
|
class Base
|
5
|
-
attr_accessor :
|
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.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
23
|
-
[:convert, :segment, :screenshot].each
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 :
|
6
|
+
attr_accessor :dry_run, :verbose
|
7
7
|
end
|
8
|
-
self.
|
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
|
-
|
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[:
|
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, :
|
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.
|
11
|
-
self.
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
28
|
+
attr_accessor :inputs, :outputs
|
17
29
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
46
|
-
if
|
47
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
58
|
+
threads.each { |t| t.join } if VideoConverter.paral
|
59
|
+
success
|
62
60
|
end
|
63
|
-
|
64
|
-
private
|
65
61
|
|
66
|
-
|
67
|
-
{ :bin => self.class.bin, :log => log }
|
68
|
-
end
|
62
|
+
private
|
69
63
|
|
70
|
-
def prepare_params
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
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 = "%{
|
9
|
+
self.metadata_command = "%{ffprobe_bin} %{input} 2>&1"
|
10
10
|
|
11
|
-
attr_accessor :input, :
|
11
|
+
attr_accessor :input, :output_groups
|
12
12
|
|
13
|
-
def initialize input
|
14
|
-
raise ArgumentError.new('input is needed') if input.
|
13
|
+
def initialize input, outputs = []
|
14
|
+
raise ArgumentError.new('input is needed') if input.blank?
|
15
15
|
self.input = input
|
16
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
81
|
-
|
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, :
|
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.
|
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.
|
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 :
|
17
|
+
attr_accessor :inputs, :outputs
|
20
18
|
|
21
|
-
def initialize
|
22
|
-
self.
|
23
|
-
|
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
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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.
|
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 = `#{
|
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 = `#{
|
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 = `#{
|
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
|
90
|
-
{
|
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
|
@@ -3,67 +3,65 @@
|
|
3
3
|
module VideoConverter
|
4
4
|
class Output
|
5
5
|
class << self
|
6
|
-
attr_accessor :
|
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 :
|
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]
|
26
|
-
raise ArgumentError.new('Incorrect type')
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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.
|
36
|
-
|
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.
|
54
|
-
self.
|
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]
|
58
|
-
self.
|
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]
|
62
|
-
self.frame_rate = 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
|
Binary file
|
Binary file
|
Binary file
|
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
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
8
|
+
context 'segmentation' do
|
9
|
+
context 'with transcoding' do
|
11
10
|
setup do
|
12
|
-
|
13
|
-
@
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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 '
|
41
|
+
context 'without transcoding' do
|
74
42
|
setup do
|
75
|
-
|
76
|
-
|
77
|
-
@
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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.
|
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-
|
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/
|
121
|
-
- test/
|
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: -
|
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: -
|
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/
|
160
|
-
- test/
|
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
|
data/test/fixtures/test.mp4
DELETED
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
|