vtools 0.0.1
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/INSTALL +0 -0
- data/LICENSE +20 -0
- data/README.md +131 -0
- data/Rakefile +29 -0
- data/bin/vtools +22 -0
- data/doc/CONFIG.md +36 -0
- data/doc/HOOKS.md +37 -0
- data/doc/LIB_EXAMPLE.md +109 -0
- data/extconf.rb +7 -0
- data/lib/vtools.rb +79 -0
- data/lib/vtools/config.rb +91 -0
- data/lib/vtools/convert_options.rb +155 -0
- data/lib/vtools/converter.rb +98 -0
- data/lib/vtools/errors.rb +21 -0
- data/lib/vtools/handler.rb +43 -0
- data/lib/vtools/harvester.rb +71 -0
- data/lib/vtools/job.rb +48 -0
- data/lib/vtools/options.rb +101 -0
- data/lib/vtools/shared_methods.rb +131 -0
- data/lib/vtools/storage.rb +67 -0
- data/lib/vtools/thumbnailer.rb +93 -0
- data/lib/vtools/thumbs_options.rb +80 -0
- data/lib/vtools/version.rb +6 -0
- data/lib/vtools/version.rb~ +4 -0
- data/lib/vtools/video.rb +158 -0
- data/setup.rb +1585 -0
- data/spec/config_spec.rb +142 -0
- data/spec/convert_options_spec.rb +284 -0
- data/spec/converter_spec.rb +167 -0
- data/spec/errors_spec.rb +39 -0
- data/spec/fixtures/outputs/file_with_iso-8859-1.txt +35 -0
- data/spec/fixtures/outputs/file_with_no_audio.txt +18 -0
- data/spec/fixtures/outputs/file_with_non_supported_audio.txt +29 -0
- data/spec/fixtures/outputs/file_with_start_value.txt +19 -0
- data/spec/fixtures/outputs/file_with_surround_sound.txt +19 -0
- data/spec/handler_spec.rb +81 -0
- data/spec/harvester_spec.rb +189 -0
- data/spec/job_spec.rb +130 -0
- data/spec/options_spec.rb +52 -0
- data/spec/shared_methods_spec.rb +351 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/storage_spec.rb +106 -0
- data/spec/thumbnailer_spec.rb +178 -0
- data/spec/thumbs_options_spec.rb +159 -0
- data/spec/video_spec.rb +274 -0
- data/vtools.gemspec +29 -0
- metadata +177 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
# default configuration options
|
4
|
+
module VTools
|
5
|
+
|
6
|
+
CONFIG = {
|
7
|
+
|
8
|
+
# system environment
|
9
|
+
:PWD => Dir.getwd,
|
10
|
+
:library => [],
|
11
|
+
:logging => nil,
|
12
|
+
:log_file => nil,
|
13
|
+
:config_file => nil,
|
14
|
+
:ffmpeg_binary => '/usr/bin/ffmpeg',
|
15
|
+
:thumb_binary => '/usr/bin/ffmpegthumbnailer',
|
16
|
+
|
17
|
+
# harvester
|
18
|
+
:max_jobs => 5,
|
19
|
+
:store_jobs => 10,
|
20
|
+
:harvester_timer => 3,
|
21
|
+
:temp_dir => '',
|
22
|
+
|
23
|
+
# converter
|
24
|
+
:video_storage => nil,
|
25
|
+
:validate_duration => nil,
|
26
|
+
# thumbnailer
|
27
|
+
:thumb_storage => nil,
|
28
|
+
|
29
|
+
# predefined video qualities
|
30
|
+
:video_set => {
|
31
|
+
# SET_NAME -vcodec VC -acodec AC -s WDTxHGT -vb BR -ab BR -ar SMPL -ac CH EXT POSTFIX -vpre CONF
|
32
|
+
:x264_180p => ['libx264', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'mp4', '_180', 'normal' ],
|
33
|
+
:x264_240p => ['libx264', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'mp4', '_240', 'normal' ],
|
34
|
+
:x264_360p => ['libx264', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'mp4', '_360', 'normal' ],
|
35
|
+
:x264_480p => ['libx264', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'mp4', '_480', 'normal' ],
|
36
|
+
:x264_720p => ['libx264', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'mp4', '_720', 'normal' ],
|
37
|
+
:x264_1080p => ['libx264', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'mp4', '_1080', 'normal' ],
|
38
|
+
|
39
|
+
:mp4_180p => ['mpeg4', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'mp4', '_180', ],
|
40
|
+
:mp4_240p => ['mpeg4', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'mp4', '_240', ],
|
41
|
+
:mp4_360p => ['mpeg4', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'mp4', '_360', ],
|
42
|
+
:mp4_480p => ['mpeg4', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'mp4', '_480', ],
|
43
|
+
:mp4_720p => ['mpeg4', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'mp4', '_720', ],
|
44
|
+
:mp4_1080p => ['mpeg4', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'mp4', '_1080', ],
|
45
|
+
|
46
|
+
:flv_180p => ['flv', 'libfaac', '240x180', '96k', '64k', 22050, 2, 'flv', '_180', ],
|
47
|
+
:flv_240p => ['flv', 'libfaac', '426x240', '128k', '64k', 22050, 2, 'flv', '_240', ],
|
48
|
+
:flv_360p => ['flv', 'libfaac', '640x360', '480k', '128k', 44100, 2, 'flv', '_360', ],
|
49
|
+
:flv_480p => ['flv', 'libfaac', '845x480', '720k', '128k', 44100, 2, 'flv', '_480', ],
|
50
|
+
:flv_720p => ['flv', 'libfaac', '1280x720', '1024k', '128k', 44100, 2, 'flv', '_720', ],
|
51
|
+
:flv_1080p => ['flv', 'libfaac', '1920x1080', '2048k', '128k', 44100, 2, 'flv', '_1080', ],
|
52
|
+
},
|
53
|
+
|
54
|
+
# predefined thumbnailer setup
|
55
|
+
:thumb_set => {
|
56
|
+
# -s -q count start%
|
57
|
+
:w120 => [120, 10, 5, 0],
|
58
|
+
:w240 => [240, 10, 5, 0],
|
59
|
+
:w360 => [360, 10, 5, 0],
|
60
|
+
:w360 => [480, 10, 5, 0],
|
61
|
+
:w600 => [600, 10, 5, 0],
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
# parse external config file
|
66
|
+
def CONFIG.load!
|
67
|
+
begin
|
68
|
+
data = YAML.load_file self[:config_file]
|
69
|
+
data = VTools.keys_to_sym data
|
70
|
+
append! data
|
71
|
+
rescue => e
|
72
|
+
raise ConfigError, "Invalid config data #{e}"
|
73
|
+
end if self[:config_file]
|
74
|
+
end
|
75
|
+
|
76
|
+
# merge config data
|
77
|
+
def CONFIG.append! data
|
78
|
+
direct = [:ffmpeg_binary, :thumb_binary, :max_jobs, :store_jobs,
|
79
|
+
:harvester_timer, :temp_dir, :video_storage, :thumb_storage]
|
80
|
+
# common data
|
81
|
+
merge! data.select { |key, value| direct.include?(key) }
|
82
|
+
|
83
|
+
self[:library] += data[:library] if data[:library].is_a? Array # libs
|
84
|
+
|
85
|
+
[:video_set, :thumb_set].each do |index| # predefined video & thumbs
|
86
|
+
self[index].merge! data[index] if
|
87
|
+
data[index].is_a?(Hash) &&
|
88
|
+
data[index].values.reject{ |val| val.is_a? Array}.empty?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end # VTools::CONFIG
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# options for the video converter
|
6
|
+
class ConvertOptions < Hash
|
7
|
+
include SharedMethods
|
8
|
+
|
9
|
+
def initialize options = {}
|
10
|
+
@ignore = [:width, :height, :resolution, :extension, :preserve_aspect, :duration, :postfix]
|
11
|
+
parse! options
|
12
|
+
end
|
13
|
+
|
14
|
+
# set value method
|
15
|
+
# backward compatibility for width height & resolution
|
16
|
+
def []= (key, value)
|
17
|
+
ignore = @ignore[0..2] << :s
|
18
|
+
|
19
|
+
case
|
20
|
+
# width & height
|
21
|
+
when ignore.first(2).include?(key)
|
22
|
+
ignore.last(2).each { |index| delete(index) }
|
23
|
+
data = { key => value }
|
24
|
+
# resolution
|
25
|
+
when ignore.last(2).include?(key)
|
26
|
+
ignore.each { |index| delete(index) }
|
27
|
+
width, height = value.split("x")
|
28
|
+
data = { :width => width.to_i, :height => height.to_i, :resolution => value, :s => value}
|
29
|
+
# duration
|
30
|
+
when [:duration, :t].include?(key)
|
31
|
+
data = { :duration => value, :t => value }
|
32
|
+
else
|
33
|
+
return super
|
34
|
+
end
|
35
|
+
|
36
|
+
merge! data
|
37
|
+
return value
|
38
|
+
end
|
39
|
+
|
40
|
+
# to string method
|
41
|
+
def to_s
|
42
|
+
|
43
|
+
params = collect do |key, value|
|
44
|
+
"-#{key} #{value}" unless @ignore.include?(key)
|
45
|
+
end.compact
|
46
|
+
|
47
|
+
# put the preset parameters last
|
48
|
+
params = params.reject { |p| p =~ /[avfs]pre/ } + params.select { |p| p =~ /[avfs]pre/ }
|
49
|
+
|
50
|
+
params.join " "
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
# string parser for the options
|
55
|
+
def parse! options
|
56
|
+
|
57
|
+
case
|
58
|
+
# try to convert string into valid ffmpeg values
|
59
|
+
when options.is_a?(String) && CONFIG[:video_set].include?(options.to_sym)
|
60
|
+
# get config data
|
61
|
+
vcodec, acodec, s, vb, ab, ar, ac,
|
62
|
+
extension, postfix, vpre = CONFIG[:video_set][options.to_sym]
|
63
|
+
|
64
|
+
# set storage
|
65
|
+
options = {
|
66
|
+
:vcodec => vcodec,
|
67
|
+
:acodec => acodec,
|
68
|
+
:s => s,
|
69
|
+
:vb => vb,
|
70
|
+
:ab => ab,
|
71
|
+
:ar => ar,
|
72
|
+
:ac => ac,
|
73
|
+
:postfix => postfix,
|
74
|
+
:extension => extension,
|
75
|
+
:preserve_aspect => true,
|
76
|
+
}
|
77
|
+
options[:vpre] = vpre if vpre
|
78
|
+
|
79
|
+
when !options.is_a?(Hash)
|
80
|
+
raise ConfigError, "Options should be a Hash or String (predefined set)"
|
81
|
+
else
|
82
|
+
options = keys_to_sym options
|
83
|
+
# check inline predefined
|
84
|
+
parse! options.delete(:set) if options.has_key? :set
|
85
|
+
end
|
86
|
+
|
87
|
+
perform options
|
88
|
+
merge! options
|
89
|
+
end
|
90
|
+
|
91
|
+
# correct width x height | resolution | s values
|
92
|
+
def perform hash
|
93
|
+
|
94
|
+
# fix duration
|
95
|
+
hash[:t] = hash[:duration] if hash[:duration]
|
96
|
+
|
97
|
+
dimmensions = hash[:resolution] ||
|
98
|
+
("#{hash[:width]}x#{hash[:height]}" if hash[:width] && hash[:height]) ||
|
99
|
+
hash[:s]
|
100
|
+
|
101
|
+
if dimmensions
|
102
|
+
# recreate dimmensions dimmensions
|
103
|
+
if hash[:preserve_aspect]
|
104
|
+
width, height = recalculate dimmensions
|
105
|
+
dimmensions = "#{width}x#{height}"
|
106
|
+
end
|
107
|
+
|
108
|
+
hash[:s] = dimmensions
|
109
|
+
else
|
110
|
+
hash.delete(:s)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# keep resolution
|
115
|
+
def recalculate dimm
|
116
|
+
width, height = dimm.split("x").map(&:to_f)
|
117
|
+
|
118
|
+
return [width, height] unless self[:aspect]
|
119
|
+
|
120
|
+
# width main:
|
121
|
+
if self[:aspect] > 1
|
122
|
+
# heigh = -1
|
123
|
+
resize = (width / self[:aspect]).round
|
124
|
+
resize += 1 if resize.odd? # needed if new_height ended up with no decimals in the first place
|
125
|
+
|
126
|
+
if height < resize
|
127
|
+
width = (height * self[:aspect]).round
|
128
|
+
width += 1 if width.odd?
|
129
|
+
else
|
130
|
+
height = resize
|
131
|
+
end
|
132
|
+
|
133
|
+
# height main:
|
134
|
+
elsif self[:aspect] < 1
|
135
|
+
# width = -1
|
136
|
+
resize = (height * self[:aspect]).round
|
137
|
+
resize += 1 if resize.odd?
|
138
|
+
|
139
|
+
if width < resize
|
140
|
+
height = (width / self[:aspect]).round
|
141
|
+
height += 1 if height.odd?
|
142
|
+
else
|
143
|
+
width = resize
|
144
|
+
end
|
145
|
+
|
146
|
+
# square:
|
147
|
+
else
|
148
|
+
width = height
|
149
|
+
end
|
150
|
+
|
151
|
+
[width, height].map(&:to_i)
|
152
|
+
end
|
153
|
+
|
154
|
+
end # ConverterOptions
|
155
|
+
end # VTools
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# Video converter itself
|
6
|
+
class Converter
|
7
|
+
include SharedMethods
|
8
|
+
|
9
|
+
# constructor
|
10
|
+
def initialize video
|
11
|
+
@video = video
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# ffmpeg converter cicle
|
16
|
+
#
|
17
|
+
# ffmpeg < 0.8: frame= 413 fps= 48 q=31.0 size= 2139kB time=16.52 bitrate=1060.6kbits/s
|
18
|
+
# ffmpeg >= 0.8: frame= 485 fps= 46 q=31.0 size= 45306kB time=00:02:42.28 bitrate=2287.0kbits/
|
19
|
+
def run
|
20
|
+
|
21
|
+
@options = @video.convert_options
|
22
|
+
@output_file = "#{generate_path @video.name}/#{@video.name}#{@options[:postfix]}.#{@options[:extension]}"
|
23
|
+
|
24
|
+
command = "#{CONFIG[:ffmpeg_binary]} -y -i '#{@video.path}' #{@options} '#{@output_file}'"
|
25
|
+
output = ""
|
26
|
+
convert_error = true
|
27
|
+
|
28
|
+
# before convert callbacks
|
29
|
+
Handler.exec :before_convert, @video, command
|
30
|
+
|
31
|
+
# process video
|
32
|
+
Open3.popen3(command) do |stdin, stdout, stderr|
|
33
|
+
stderr.each "r" do |line|
|
34
|
+
VTools.fix_encoding line
|
35
|
+
output << line
|
36
|
+
|
37
|
+
# we know, that all is not so bad, if "time=" at least once met
|
38
|
+
if line.include? "time="
|
39
|
+
|
40
|
+
convert_error = false # that is why, we say "generally it's OK"
|
41
|
+
|
42
|
+
if line =~ /time=(\d+):(\d+):(\d+.\d+)/ # ffmpeg 0.8 and above style
|
43
|
+
time = ($1.to_i * 3600) + ($2.to_i * 60) + $3.to_f
|
44
|
+
elsif line =~ /time=(\d+.\d+)/ # ffmpeg 0.7 and below style
|
45
|
+
time = $1.to_f
|
46
|
+
else # in case of unexpected output
|
47
|
+
time = 0.0
|
48
|
+
end
|
49
|
+
progress = time / @video.duration
|
50
|
+
|
51
|
+
# callbacks
|
52
|
+
Handler.exec :in_convert, @video, progress
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
raise ProcessError, output.split("\n").last if convert_error # exit on error
|
58
|
+
|
59
|
+
# callbacks
|
60
|
+
unless error = encoding_invalid?
|
61
|
+
Handler.exec :convert_success, @video, @output_file
|
62
|
+
else
|
63
|
+
Handler.exec :convert_error, @video, error, output
|
64
|
+
raise ProcessError, error # raise exception in error
|
65
|
+
end
|
66
|
+
|
67
|
+
encoded
|
68
|
+
end
|
69
|
+
|
70
|
+
# define if encoded succeed
|
71
|
+
def encoding_invalid?
|
72
|
+
unless File.exists?(@output_file)
|
73
|
+
return "No output file created"
|
74
|
+
end
|
75
|
+
|
76
|
+
unless encoded.valid?
|
77
|
+
return "Encoded file is invalid"
|
78
|
+
end
|
79
|
+
|
80
|
+
if CONFIG[:validate_duration]
|
81
|
+
# reavalidate duration
|
82
|
+
precision = @options[:duration] ? 1.5 : 1.1
|
83
|
+
desired_duration = @options[:duration] && @options[:duration] < @video.duration ? @options[:duration] : @video.duration
|
84
|
+
|
85
|
+
if (encoded.duration >= (desired_duration * precision) or encoded.duration <= (desired_duration / precision))
|
86
|
+
return "Encoded file duration is invalid (original/specified: #{desired_duration}sec, got: #{encoded.duration}sec)"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
# encoded media
|
94
|
+
def encoded
|
95
|
+
@encoded ||= Video.new(@output_file).get_info
|
96
|
+
end
|
97
|
+
end # Converter
|
98
|
+
end # VTools
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
# VTools exceptions
|
4
|
+
module VTools
|
5
|
+
|
6
|
+
# confuguration error
|
7
|
+
class ConfigError < ArgumentError
|
8
|
+
end
|
9
|
+
|
10
|
+
# specified file does not exist
|
11
|
+
class FileError < Errno::ENOENT
|
12
|
+
end
|
13
|
+
|
14
|
+
# invalid video format
|
15
|
+
class FormatError < IOError
|
16
|
+
end
|
17
|
+
|
18
|
+
# invalid video format
|
19
|
+
class ProcessError < IOError
|
20
|
+
end
|
21
|
+
end # VTools
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# callback handler
|
6
|
+
# allows to execute external script callbacks
|
7
|
+
# multiple callbacs in one placeholder are allowed
|
8
|
+
#
|
9
|
+
# usage:
|
10
|
+
# Handler.set :placeholder_name, &block
|
11
|
+
# or
|
12
|
+
# Handler.collection do
|
13
|
+
# set :placeholder_one, &block
|
14
|
+
# set :placeholder_other, &block
|
15
|
+
# end
|
16
|
+
class Handler
|
17
|
+
include SharedMethods
|
18
|
+
|
19
|
+
@callbacks = {}
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# callbacks setter
|
23
|
+
def set action, &block
|
24
|
+
action = action.to_sym
|
25
|
+
@callbacks[action] = [] unless @callbacks[action].is_a? Array
|
26
|
+
@callbacks[action] << block if block_given?
|
27
|
+
end
|
28
|
+
|
29
|
+
# pending callbacks exectuion
|
30
|
+
def exec action, *args
|
31
|
+
action = action.to_sym
|
32
|
+
@callbacks[action].each do |block|
|
33
|
+
block.call(*args)
|
34
|
+
end if @callbacks[action].is_a? Array
|
35
|
+
end
|
36
|
+
|
37
|
+
# collection setup
|
38
|
+
def collection &block
|
39
|
+
instance_eval &block if block_given?
|
40
|
+
end
|
41
|
+
end # << self
|
42
|
+
end # Handler
|
43
|
+
end # VTools
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module VTools
|
4
|
+
|
5
|
+
# Takes care about jobs
|
6
|
+
class Harvester
|
7
|
+
include SharedMethods
|
8
|
+
@jobs = {}
|
9
|
+
@run_jobs = 0
|
10
|
+
|
11
|
+
# get job info
|
12
|
+
class << self
|
13
|
+
|
14
|
+
# collector
|
15
|
+
def daemonize!
|
16
|
+
|
17
|
+
Storage.connect # connect jobs pool
|
18
|
+
loop do
|
19
|
+
|
20
|
+
with_error_handle do # catch job exceptions
|
21
|
+
config = json_to_obj Storage.recv
|
22
|
+
add_job config
|
23
|
+
end if CONFIG[:max_jobs] > @run_jobs
|
24
|
+
|
25
|
+
sleep CONFIG[:harvester_timer]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# set new job
|
30
|
+
def add_job config
|
31
|
+
|
32
|
+
job = Job.new config
|
33
|
+
|
34
|
+
@jobs[job.id] = job
|
35
|
+
@run_jobs += 1
|
36
|
+
|
37
|
+
# execute job
|
38
|
+
Thread.new(job, config) do
|
39
|
+
# catch job exceptions here
|
40
|
+
with_error_handle do # catch job exceptions
|
41
|
+
Storage.send({ :data => job.execute, :action => config.action })
|
42
|
+
end
|
43
|
+
finish job # in any case close job instance
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
# job terminator
|
49
|
+
def finish job
|
50
|
+
job = job.id if job.is_a? Job
|
51
|
+
return unless @jobs.has_key? job
|
52
|
+
@run_jobs -= 1
|
53
|
+
@jobs.delete job
|
54
|
+
end
|
55
|
+
|
56
|
+
# error handler
|
57
|
+
def with_error_handle &block
|
58
|
+
# catch job exceptions here
|
59
|
+
begin
|
60
|
+
yield if block_given?
|
61
|
+
# configuration, create video after convert, valid video file & process
|
62
|
+
rescue ConfigError, FileError, FormatError, ProcessError => e
|
63
|
+
log :error, "JOB rejected, #{e}"
|
64
|
+
rescue => e # uncknown error
|
65
|
+
log :fatal, "#{e}"
|
66
|
+
raise e
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end # << class
|
70
|
+
end # Harvester
|
71
|
+
end # VTools
|