video_converter 0.1.1 → 0.2.0
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.
- 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
|