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