vcs_ruby 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/vcs.rb +145 -0
- data/lib/capturer.rb +35 -0
- data/lib/command.rb +37 -0
- data/lib/configuration.rb +103 -0
- data/lib/contact_sheet.rb +341 -0
- data/lib/defaults.yml +35 -0
- data/lib/ffmpeg.rb +25 -0
- data/lib/font.rb +31 -0
- data/lib/libav.rb +144 -0
- data/lib/mplayer.rb +19 -0
- data/lib/thumbnail.rb +105 -0
- data/lib/time_index.rb +121 -0
- data/lib/tools.rb +67 -0
- data/lib/vcs.rb +14 -0
- data/lib/version.info +1 -0
- data/lib/version.rb +25 -0
- metadata +80 -0
data/bin/vcs.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Video Contact Sheet Ruby:
|
4
|
+
# ----------------------
|
5
|
+
#
|
6
|
+
# Generates contact sheets of videos
|
7
|
+
#
|
8
|
+
# Prerequisites: Ruby, ImageMagick, ffmpeg/libav or mplayer
|
9
|
+
#
|
10
|
+
|
11
|
+
# Load library path for development
|
12
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../lib")
|
13
|
+
|
14
|
+
require 'optparse'
|
15
|
+
require 'vcs'
|
16
|
+
require 'yaml'
|
17
|
+
|
18
|
+
include VCSRuby
|
19
|
+
|
20
|
+
|
21
|
+
# Configuration can Override options
|
22
|
+
options =
|
23
|
+
{
|
24
|
+
quiet: false,
|
25
|
+
verbose: false,
|
26
|
+
capturer: :any,
|
27
|
+
format: nil,
|
28
|
+
output: []
|
29
|
+
}
|
30
|
+
|
31
|
+
# Command Line Parameter arguments
|
32
|
+
|
33
|
+
arguments =
|
34
|
+
{
|
35
|
+
'--capturer' => [:ffmpeg, :libav, :mplayer, :any],
|
36
|
+
'--format' => [:png, :jpg, :jpeg, :tiff],
|
37
|
+
'--funky' => [:polaroid, :photos, :overlap, :rotate, :photoframe, :polaroidframe, :film, :random]
|
38
|
+
}
|
39
|
+
|
40
|
+
# Command Line Parameters
|
41
|
+
optparse = OptionParser.new do|opts|
|
42
|
+
opts.separator $vcs_ruby_name + ' ' + $vcs_ruby_version.to_s
|
43
|
+
opts.separator ''
|
44
|
+
opts.on( '-i [INTERVAL]', '--interval [INTERVAL]', 'Set the interval [INTERVAL]') do |interval|
|
45
|
+
options[:interval] = TimeIndex.new interval
|
46
|
+
end
|
47
|
+
opts.on( '-c [COLMNS]', '--columns [COLUMNS]', 'Arrange the output in <COLUMNS> columns.') do |columns|
|
48
|
+
options[:columns] = columns.to_i
|
49
|
+
end
|
50
|
+
opts.on( '-r [ROWS]', '--rows [ROWS]', 'Arrange the output in <ROWS> rows.') do |rows|
|
51
|
+
options[:rows] = rows.to_i
|
52
|
+
end
|
53
|
+
opts.on( '-H [HEIGHT]', '--height [HEIGHT]', 'Set the output (individual thumbnail) height.') do |height|
|
54
|
+
options[:height] = height.to_i
|
55
|
+
end
|
56
|
+
opts.on( '-W [WIDTH]', '--width [WIDTH]', 'Set the output (individual thumbnail) width.') do |width|
|
57
|
+
options[:width] = width.to_i
|
58
|
+
end
|
59
|
+
opts.on( '-A [ASPECT]', '--aspect [ASPECT]', 'Aspect ratio. Accepts a floating point number or a fraction.') do |aspect|
|
60
|
+
options[:aspect] = aspect.to_f
|
61
|
+
end
|
62
|
+
opts.on( '-f [FROM]', '--from [FROM]', 'Set starting time. No caps before this.') do |from|
|
63
|
+
options[:from] = TimeIndex.new from
|
64
|
+
end
|
65
|
+
opts.on( '-t [TO]', '--to [TO]', 'Set ending time. No caps beyond this.') do |to|
|
66
|
+
options[:to] = TimeIndex.new to
|
67
|
+
end
|
68
|
+
opts.on( '-T [TITLE]', '--title [TITLE]', 'Set ending time. No caps beyond this.') do |title|
|
69
|
+
options[:title] = title
|
70
|
+
end
|
71
|
+
opts.on( '-f [format]', '--format [FORMAT]', arguments['--format'], 'Formats: ' + Tools::list_arguments(arguments["--format"])) do |format|
|
72
|
+
options[:format] = format
|
73
|
+
end
|
74
|
+
opts.on('-C [CAPTURER]', '--capture [CAPTURER]', arguments['--capturer'], 'Capturer: ' + Tools::list_arguments(arguments["--capturer"])) do |capturer|
|
75
|
+
options[:capturer] = capturer
|
76
|
+
end
|
77
|
+
opts.on( '-T [TITLE]', '--title [TITLE]', 'Set ending time. No caps beyond this.') do |title|
|
78
|
+
options[:title] = title
|
79
|
+
end
|
80
|
+
opts.on( '-o [FILE]', '--output [FILE]', 'File name of output. When ommited will be derived from the input filename. Can be repeated for multiple files.') do |file|
|
81
|
+
options[:output] << file
|
82
|
+
end
|
83
|
+
opts.on( '-s [SIGNATURE]', '--signature [SIGNATURE]', 'Change the image signature to your preference.') do |signature|
|
84
|
+
options[:signature] = signature
|
85
|
+
end
|
86
|
+
opts.on( '--no-signature', 'Remove footer with signature') do
|
87
|
+
options[:no_signature] = true
|
88
|
+
end
|
89
|
+
opts.on( '-l [HIGHLIGHT]', '--highlight [HIGHLIGHT]' 'Add the frame found at timestamp [HIGHLIGHT] as a highlight.') do |highlight|
|
90
|
+
options[:highlight] = TimeIndex.new highlight
|
91
|
+
end
|
92
|
+
opts.on( '-q', '--quiet', 'Don\'t print progress messages just errors. Repeat to mute completely, even on error.') do |file|
|
93
|
+
options[:quiet] = true
|
94
|
+
end
|
95
|
+
opts.on("-V", "--verbose", "More verbose Output.") do
|
96
|
+
options[:verbose] = true
|
97
|
+
end
|
98
|
+
opts.on( '-v', '--version', 'Version' ) do
|
99
|
+
puts $vcs_ruby_name + ' ' + $vcs_ruby_version.to_s
|
100
|
+
exit 0
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on( '-h', '--help', 'Prints help' ) do
|
104
|
+
options[:help] = true
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.separator ''
|
108
|
+
opts.separator 'Examples:'
|
109
|
+
opts.separator ' Create a contact sheet with default values (4 x 4 matrix):'
|
110
|
+
opts.separator ' $ vcs video.avi'
|
111
|
+
opts.separator ''
|
112
|
+
opts.separator ' Create a sheet with vidcaps at intervals of 3 and a half minutes, save to'
|
113
|
+
opts.separator ' "output.jpg":'
|
114
|
+
opts.separator ' $ vcs -i 3m30 input.wmv -o output.jpg'
|
115
|
+
opts.separator ''
|
116
|
+
opts.separator ' Create a sheet with vidcaps starting at 3 mins and ending at 18 mins in 2m intervals'
|
117
|
+
opts.separator ' $ vcs --from 3m --to 18m -i 2m input.avi'
|
118
|
+
opts.separator ''
|
119
|
+
opts.separator ' See more examples at vcs-ruby homepage <https://github.com/FreeApophis/vcs.rb>.'
|
120
|
+
opts.separator ''
|
121
|
+
end
|
122
|
+
|
123
|
+
Tools::print_help optparse if ARGV.empty?
|
124
|
+
|
125
|
+
optparse.parse!
|
126
|
+
|
127
|
+
puts options.inspect
|
128
|
+
|
129
|
+
Tools::print_help optparse if options[:help] || ARGV.empty?
|
130
|
+
|
131
|
+
Tools::verbose = options[:verbose]
|
132
|
+
Tools::quiet = options[:quiet]
|
133
|
+
|
134
|
+
# Invoke ContactSheet
|
135
|
+
|
136
|
+
begin
|
137
|
+
ARGV.each_with_index do |video, index|
|
138
|
+
sheet = Tools::contact_sheet_with_options video, options
|
139
|
+
sheet.initialize_filename(options[:output][index]) if options[:output][index]
|
140
|
+
sheet.build
|
141
|
+
end
|
142
|
+
rescue Exception => e
|
143
|
+
STDERR.puts "ERROR: #{e.message}"
|
144
|
+
end
|
145
|
+
|
data/lib/capturer.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# Capturer Baseclass
|
3
|
+
#
|
4
|
+
|
5
|
+
module VCSRuby
|
6
|
+
class Capturer
|
7
|
+
def available?
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
raise "NotImplmentedException"
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_video
|
16
|
+
raise "NotImplmentedException"
|
17
|
+
end
|
18
|
+
|
19
|
+
def length
|
20
|
+
raise "NotImplmentedException"
|
21
|
+
end
|
22
|
+
|
23
|
+
def width
|
24
|
+
raise "NotImplmentedException"
|
25
|
+
end
|
26
|
+
|
27
|
+
def height
|
28
|
+
raise "NotImplmentedException"
|
29
|
+
end
|
30
|
+
|
31
|
+
def grab time
|
32
|
+
raise "NotImplmentedException"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/command.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#
|
2
|
+
# Command
|
3
|
+
#
|
4
|
+
|
5
|
+
module VCSRuby
|
6
|
+
class Command
|
7
|
+
attr_reader :name, :available
|
8
|
+
def initialize name, command
|
9
|
+
@name = name
|
10
|
+
@command = which(command)
|
11
|
+
@available = !!@command
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute parameter, streams = "2> /dev/null"
|
15
|
+
raise "Command '#{name}' not available" unless available
|
16
|
+
|
17
|
+
if Tools::windows?
|
18
|
+
`cmd /C #{@command} #{parameter}`
|
19
|
+
else
|
20
|
+
`#{@command} #{parameter} #{streams}`
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
# http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
26
|
+
def which cmd
|
27
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
28
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
29
|
+
exts.each do |ext|
|
30
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
31
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
#
|
2
|
+
# Configuration
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'font'
|
6
|
+
|
7
|
+
module VCSRuby
|
8
|
+
class Configuration
|
9
|
+
attr_accessor :capturer
|
10
|
+
attr_reader :header_font, :title_font, :timestamp_font, :signature_font
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
default_config_file = File.expand_path("defaults.yml", File.dirname(__FILE__))
|
14
|
+
local_config_files = ['~/.vcs.rb.yml']
|
15
|
+
|
16
|
+
config = ::YAML::load_file(default_config_file)
|
17
|
+
local_config_files.select{ |f| File.exists?(f) }.each do |local_config_file|
|
18
|
+
puts "Local configuration file loaded: #{local_config_file}" if Tools.verbose?
|
19
|
+
local_config = YAML::load_file(local_config_file)
|
20
|
+
cconfig.merge(local_config)
|
21
|
+
end
|
22
|
+
|
23
|
+
@config = config
|
24
|
+
|
25
|
+
@header_font = Font.new @config['style']['header']['font'], @config['style']['header']['size']
|
26
|
+
@title_font = Font.new @config['style']['title']['font'], @config['style']['title']['size']
|
27
|
+
@timestamp_font = Font.new @config['style']['timestamp']['font'], @config['style']['timestamp']['size']
|
28
|
+
@signature_font = Font.new @config['style']['signature']['font'], @config['style']['signature']['size']
|
29
|
+
end
|
30
|
+
|
31
|
+
def rows
|
32
|
+
@config['main']['rows'] ? @config['main']['rows'].to_i : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def columns
|
36
|
+
@config['main']['columns'] ? @config['main']['columns'].to_i : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def interval
|
40
|
+
@config['main']['interval'] ? TimeIndex.new(@config['main']['interval']) : nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def padding
|
44
|
+
@config['main']['padding'] ? @config['main']['padding'].to_i : 2
|
45
|
+
end
|
46
|
+
|
47
|
+
def quality
|
48
|
+
@config['main']['quality'] ? @config['main']['quality'].to_i : 90
|
49
|
+
end
|
50
|
+
|
51
|
+
def header_background
|
52
|
+
@config['style']['header']['background']
|
53
|
+
end
|
54
|
+
|
55
|
+
def header_color
|
56
|
+
@config['style']['header']['color']
|
57
|
+
end
|
58
|
+
|
59
|
+
def title_background
|
60
|
+
@config['style']['title']['background']
|
61
|
+
end
|
62
|
+
|
63
|
+
def title_color
|
64
|
+
@config['style']['title']['color']
|
65
|
+
end
|
66
|
+
|
67
|
+
def highlight_background
|
68
|
+
@config['style']['highlight']['background']
|
69
|
+
end
|
70
|
+
|
71
|
+
def contact_background
|
72
|
+
@config['style']['contact']['background']
|
73
|
+
end
|
74
|
+
|
75
|
+
def timestamp_background
|
76
|
+
@config['style']['timestamp']['background']
|
77
|
+
end
|
78
|
+
|
79
|
+
def timestamp_color
|
80
|
+
@config['style']['timestamp']['color']
|
81
|
+
end
|
82
|
+
|
83
|
+
def signature_background
|
84
|
+
@config['style']['signature']['background']
|
85
|
+
end
|
86
|
+
|
87
|
+
def signature_color
|
88
|
+
@config['style']['signature']['color']
|
89
|
+
end
|
90
|
+
|
91
|
+
def blank_threshold
|
92
|
+
@config['lowlevel']['blank_threshold'].to_f
|
93
|
+
end
|
94
|
+
|
95
|
+
def blank_evasion?
|
96
|
+
@config['lowlevel']['blank_evasion']
|
97
|
+
end
|
98
|
+
|
99
|
+
def blank_alternatives
|
100
|
+
@config['lowlevel']['blank_alternatives'].map{ |e| TimeIndex.new e.to_i }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,341 @@
|
|
1
|
+
#
|
2
|
+
# Contact Sheet Composited from the Thumbnails
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
require 'vcs'
|
10
|
+
|
11
|
+
module VCSRuby
|
12
|
+
class ContactSheet
|
13
|
+
attr_accessor :capturer, :format, :signature, :title, :highlight
|
14
|
+
attr_reader :thumbnail_width, :thumbnail_height
|
15
|
+
attr_reader :length, :from, :to
|
16
|
+
|
17
|
+
def initialize video, capturer = :any
|
18
|
+
@capturer = capturer
|
19
|
+
@configuration = Configuration.new
|
20
|
+
@signature = "Created by Video Contact Sheet Ruby"
|
21
|
+
initialize_capturers video
|
22
|
+
initialize_filename(File.basename(@video, '.*'))
|
23
|
+
puts "Processing #{File.basename(video)}..." unless Tools.quiet?
|
24
|
+
detect_video_properties
|
25
|
+
|
26
|
+
@thumbnails = []
|
27
|
+
|
28
|
+
@tempdir = Dir.mktmpdir
|
29
|
+
|
30
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@tempdir) )
|
31
|
+
|
32
|
+
initialize_geometry(@configuration.rows, @configuration.columns, @configuration.interval)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize_filename filename
|
36
|
+
@out_path = File.dirname(filename)
|
37
|
+
@out_filename = File.basename(filename,'.*')
|
38
|
+
ext = File.extname(filename).gsub('.', '')
|
39
|
+
if ['png', 'jpg', 'jpeg', 'tiff'].include?(ext)
|
40
|
+
@format ||= ext.to_sym
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def filename
|
45
|
+
"#{@out_filename}.#{@format ? @format.to_s : 'png'}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def full_path
|
49
|
+
File.join(@out_path, filename)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize_geometry(rows, columns, interval)
|
53
|
+
@has_interval = !!interval
|
54
|
+
@rows = rows
|
55
|
+
@columns = columns
|
56
|
+
@interval = interval
|
57
|
+
end
|
58
|
+
|
59
|
+
def rows
|
60
|
+
@rows
|
61
|
+
end
|
62
|
+
|
63
|
+
def columns
|
64
|
+
@columns
|
65
|
+
end
|
66
|
+
|
67
|
+
def interval
|
68
|
+
@interval || (@to - @from) / (number_of_caps + 1)
|
69
|
+
end
|
70
|
+
|
71
|
+
def number_of_caps
|
72
|
+
if @has_interval
|
73
|
+
(@to - @from) / @interval
|
74
|
+
else
|
75
|
+
if @rows && @columns
|
76
|
+
@rows * @columns
|
77
|
+
else
|
78
|
+
raise "you need at least 2 parameters from columns, rows and interval"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def thumbnail_width= width
|
84
|
+
@thumbnail_height = (width.to_f / @thumbnail_width * thumbnail_height).to_i
|
85
|
+
@thumbnail_width = width
|
86
|
+
end
|
87
|
+
|
88
|
+
def thumbnail_height= height
|
89
|
+
@thumbnail_width = (height.to_f / @thumbnail_height * thumbnail_width).to_i
|
90
|
+
@thumbnail_height = height
|
91
|
+
end
|
92
|
+
|
93
|
+
def from= time
|
94
|
+
if (TimeIndex.new(0) < time) && (time < to) && (time < @length)
|
95
|
+
@from = time
|
96
|
+
else
|
97
|
+
raise "Invalid From Time"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def to= time
|
102
|
+
if (TimeIndex.new(0) < time) && (from < time) && (time < @length)
|
103
|
+
@to = time
|
104
|
+
else
|
105
|
+
raise "Invalid To Time"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def self.finalize(tempdir)
|
111
|
+
proc do
|
112
|
+
puts "Cleaning up..." unless Tools.quiet?
|
113
|
+
FileUtils.rm_r tempdir
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def build
|
118
|
+
initialize_thumbnails
|
119
|
+
capture_thumbnails
|
120
|
+
|
121
|
+
puts "Composing standard contact sheet..." unless Tools.quiet?
|
122
|
+
s = splice_montage(montage_thumbs)
|
123
|
+
|
124
|
+
image = MiniMagick::Image.open(s)
|
125
|
+
|
126
|
+
puts "Adding header and footer..." unless Tools.quiet?
|
127
|
+
final = add_header_and_footer image
|
128
|
+
|
129
|
+
puts "Done. Output wrote to '#{filename}'" unless Tools.quiet?
|
130
|
+
FileUtils.mv(final, full_path)
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
private
|
135
|
+
def selected_capturer
|
136
|
+
result = nil
|
137
|
+
if @capturer == nil || @capturer == :any
|
138
|
+
result = @capturers.first
|
139
|
+
else
|
140
|
+
result = @capturers.select{ |c| c.name == @capturer }.first
|
141
|
+
end
|
142
|
+
raise "Selected Capturer (#{@capturer.to_s}) not available" unless result
|
143
|
+
return result
|
144
|
+
end
|
145
|
+
|
146
|
+
def initialize_capturers video
|
147
|
+
capturers = []
|
148
|
+
capturers << LibAV.new(video)
|
149
|
+
capturers << MPlayer.new(video)
|
150
|
+
capturers << FFmpeg.new(video)
|
151
|
+
|
152
|
+
@video = video
|
153
|
+
@capturers = capturers.select{ |c| c.available? }
|
154
|
+
|
155
|
+
puts "Available capturers: #{@capturers.map{ |c| c.to_s }.join(', ')}" if Tools.verbose?
|
156
|
+
end
|
157
|
+
|
158
|
+
def initialize_thumbnails
|
159
|
+
time = @from
|
160
|
+
(1..number_of_caps).each do |i|
|
161
|
+
thumb = Thumbnail.new selected_capturer, @video, @configuration
|
162
|
+
|
163
|
+
thumb.width = thumbnail_width
|
164
|
+
thumb.height = thumbnail_height
|
165
|
+
thumb.time = (time += interval)
|
166
|
+
thumb.image_path = File::join(@tempdir, "th#{"%03d" % i}.png")
|
167
|
+
|
168
|
+
@thumbnails << thumb
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def capture_thumbnails
|
173
|
+
puts "Capturing in range [#{from}..#{to}]. Total length: #{@length}" unless Tools.quiet?
|
174
|
+
|
175
|
+
@thumbnails.each_with_index do |thumbnail, i|
|
176
|
+
puts "Generating capture ##{i + 1}/#{number_of_caps} #{thumbnail.time}..." unless Tools::quiet?
|
177
|
+
if @configuration.blank_evasion?
|
178
|
+
thumbnail.capture_and_evade interval
|
179
|
+
else
|
180
|
+
thumbnail.capture
|
181
|
+
end
|
182
|
+
thumbnail.apply_filters
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def detect_video_properties
|
187
|
+
detect_length
|
188
|
+
detect_dimensions
|
189
|
+
end
|
190
|
+
|
191
|
+
def detect_length
|
192
|
+
@length = selected_capturer.length
|
193
|
+
|
194
|
+
@from = TimeIndex.new 0.0
|
195
|
+
@to = @length
|
196
|
+
end
|
197
|
+
|
198
|
+
def detect_dimensions
|
199
|
+
@thumbnail_width = selected_capturer.width
|
200
|
+
@thumbnail_height = selected_capturer.height
|
201
|
+
end
|
202
|
+
|
203
|
+
def montage_thumbs
|
204
|
+
file_path = File::join(@tempdir, 'montage.png')
|
205
|
+
MiniMagick::Tool::Montage.new do |montage|
|
206
|
+
montage.background @configuration.contact_background
|
207
|
+
@thumbnails.each do |thumbnail|
|
208
|
+
montage << thumbnail.image_path
|
209
|
+
end
|
210
|
+
montage.geometry "+#{@configuration.padding}+#{@configuration.padding}"
|
211
|
+
# rows or columns can be nil (auto fit)
|
212
|
+
montage.tile "#{@columns}x#{@rows}"
|
213
|
+
montage << file_path
|
214
|
+
end
|
215
|
+
return file_path
|
216
|
+
end
|
217
|
+
|
218
|
+
def splice_montage montage_path
|
219
|
+
file_path = File::join(@tempdir, 'spliced.png')
|
220
|
+
MiniMagick::Tool::Convert.new do |convert|
|
221
|
+
convert << montage_path
|
222
|
+
convert.background @configuration.contact_background
|
223
|
+
convert.splice '5x10'
|
224
|
+
convert << file_path
|
225
|
+
end
|
226
|
+
file_path
|
227
|
+
end
|
228
|
+
|
229
|
+
def create_title montage
|
230
|
+
file_path = File::join(@tempdir, 'title.png')
|
231
|
+
MiniMagick::Tool::Convert.new do |convert|
|
232
|
+
convert.stack do |ul|
|
233
|
+
ul.size "#{montage.width}x#{@configuration.title_font.line_height}"
|
234
|
+
ul.xc @configuration.title_background
|
235
|
+
ul.font @configuration.title_font.path
|
236
|
+
ul.pointsize @configuration.title_font.size
|
237
|
+
ul.background @configuration.title_background
|
238
|
+
ul.fill @configuration.title_color
|
239
|
+
ul.gravity 'Center'
|
240
|
+
ul.annotate(0, @title)
|
241
|
+
end
|
242
|
+
convert.flatten
|
243
|
+
convert << file_path
|
244
|
+
end
|
245
|
+
return file_path
|
246
|
+
end
|
247
|
+
|
248
|
+
def create_highlight montage
|
249
|
+
puts "Generating highlight..."
|
250
|
+
thumb = Thumbnail.new selected_capturer, @video, @configuration
|
251
|
+
|
252
|
+
thumb.width = thumbnail_width
|
253
|
+
thumb.height = thumbnail_height
|
254
|
+
thumb.time = @highlight
|
255
|
+
thumb.image_path = File::join(@tempdir, "highlight_thumb.png")
|
256
|
+
thumb.capture
|
257
|
+
thumb.apply_filters
|
258
|
+
|
259
|
+
file_path = File::join(@tempdir, "highlight.png")
|
260
|
+
MiniMagick::Tool::Convert.new do |convert|
|
261
|
+
convert.stack do |a|
|
262
|
+
a.size "#{montage.width}x#{thumbnail_height+20}"
|
263
|
+
a.xc @configuration.highlight_background
|
264
|
+
a.gravity 'Center'
|
265
|
+
a << thumb.image_path
|
266
|
+
a.composite
|
267
|
+
end
|
268
|
+
convert.stack do |a|
|
269
|
+
a.size "#{montage.width}x1"
|
270
|
+
a.xc 'Black'
|
271
|
+
end
|
272
|
+
convert.append
|
273
|
+
convert << file_path
|
274
|
+
end
|
275
|
+
|
276
|
+
file_path
|
277
|
+
end
|
278
|
+
|
279
|
+
def add_header_and_footer montage
|
280
|
+
file_path = File::join(@tempdir, filename)
|
281
|
+
header_height = @configuration.header_font.line_height * 3
|
282
|
+
signature_height = @configuration.signature_font.line_height + 8
|
283
|
+
MiniMagick::Tool::Convert.new do |convert|
|
284
|
+
convert.stack do |a|
|
285
|
+
a.size "#{montage.width - 18}x1"
|
286
|
+
a.xc @configuration.header_background
|
287
|
+
a.size.+
|
288
|
+
a.font @configuration.header_font.path
|
289
|
+
a.pointsize @configuration.header_font.size
|
290
|
+
a.background @configuration.header_background
|
291
|
+
a.fill 'Black'
|
292
|
+
a.stack do |b|
|
293
|
+
b.gravity 'West'
|
294
|
+
b.stack do |c|
|
295
|
+
c.label 'Filename: '
|
296
|
+
c.font @configuration.header_font.path
|
297
|
+
c.label File.basename(@video)
|
298
|
+
c.append.+
|
299
|
+
end
|
300
|
+
b.font @configuration.header_font.path
|
301
|
+
b.label "File size: #{Tools.to_human_size(File.size(@video))}"
|
302
|
+
b.label "Length: #{@length.to_timestamp}"
|
303
|
+
b.append
|
304
|
+
b.crop "#{montage.width}x#{header_height}+0+0"
|
305
|
+
end
|
306
|
+
a.append
|
307
|
+
a.stack do |b|
|
308
|
+
b.size "#{montage.width}x#{header_height}"
|
309
|
+
b.gravity 'East'
|
310
|
+
b.fill @configuration.header_color
|
311
|
+
b.annotate '+0-1'
|
312
|
+
b << "Dimensions: #{selected_capturer.width}x#{selected_capturer.height}\nFormat: #{selected_capturer.video_codec} / #{selected_capturer.audio_codec}\nFPS: #{"%.02f" % selected_capturer.fps}"
|
313
|
+
end
|
314
|
+
a.bordercolor @configuration.header_background
|
315
|
+
a.border 9
|
316
|
+
end
|
317
|
+
convert << create_title(montage) if @title
|
318
|
+
convert << create_highlight(montage) if @highlight
|
319
|
+
convert << montage.path
|
320
|
+
convert.append
|
321
|
+
if @signature
|
322
|
+
convert.stack do |a|
|
323
|
+
a.size "#{montage.width}x#{signature_height}"
|
324
|
+
a.gravity 'Center'
|
325
|
+
a.xc @configuration.signature_background
|
326
|
+
a.font @configuration.signature_font.path
|
327
|
+
a.pointsize @configuration.signature_font.size
|
328
|
+
a.fill @configuration.signature_color
|
329
|
+
a.annotate(0, @signature)
|
330
|
+
end
|
331
|
+
convert.append
|
332
|
+
end
|
333
|
+
if format == :jpg || format == :jpeg
|
334
|
+
convert.quality(@configuration.quality)
|
335
|
+
end
|
336
|
+
convert << file_path
|
337
|
+
end
|
338
|
+
file_path
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
data/lib/defaults.yml
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
main:
|
2
|
+
rows: 4
|
3
|
+
columns: 4
|
4
|
+
interval: ~
|
5
|
+
padding: 2
|
6
|
+
quality: 95
|
7
|
+
style:
|
8
|
+
header:
|
9
|
+
font: DejaVu-Sans-Book
|
10
|
+
size: 14
|
11
|
+
color: Black
|
12
|
+
background: "#afcd7a"
|
13
|
+
title:
|
14
|
+
font: DejaVu-Sans-Book
|
15
|
+
size: 33
|
16
|
+
color: Black
|
17
|
+
background: White
|
18
|
+
highlight:
|
19
|
+
background: LightGoldenRod
|
20
|
+
contact:
|
21
|
+
background: White
|
22
|
+
timestamp:
|
23
|
+
font: DejaVu-Sans-Book
|
24
|
+
size: 14
|
25
|
+
color: White
|
26
|
+
background: "#000000aa"
|
27
|
+
signature:
|
28
|
+
font: DejaVu-Sans-Book
|
29
|
+
size: 10
|
30
|
+
color: Black
|
31
|
+
background: SlateGray
|
32
|
+
lowlevel:
|
33
|
+
blank_evasion: true
|
34
|
+
blank_threshold: 0.10
|
35
|
+
blank_alternatives: [ -5, 5, -10, 10, -30, 30]
|
data/lib/ffmpeg.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# FFmpeg Abstraction
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'command'
|
6
|
+
require 'capturer'
|
7
|
+
|
8
|
+
module VCSRuby
|
9
|
+
class FFmpeg < Capturer
|
10
|
+
def initialize video
|
11
|
+
@video = video
|
12
|
+
@command = Command.new :ffmpeg, 'ffmpeg'
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
:ffmpeg
|
17
|
+
end
|
18
|
+
|
19
|
+
def length
|
20
|
+
info = @command.execute("-i #{@video} -dframes 0 -vframes 0 /dev/null", "2>&1")
|
21
|
+
match = /Duration: ([\d|:|.]*)/.match(info)
|
22
|
+
return TimeIndex.new match[1]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/font.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Font helper
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'mini_magick'
|
6
|
+
|
7
|
+
module VCSRuby
|
8
|
+
class Font
|
9
|
+
attr_reader :name, :path, :size
|
10
|
+
|
11
|
+
def initialize name, size
|
12
|
+
@name = name
|
13
|
+
@path = find_path
|
14
|
+
@size = size
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_path
|
18
|
+
'/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf'
|
19
|
+
end
|
20
|
+
|
21
|
+
def line_height
|
22
|
+
MiniMagick::Tool::Convert.new do |convert|
|
23
|
+
convert.font path
|
24
|
+
convert.pointsize size
|
25
|
+
convert << 'label:F'
|
26
|
+
convert.format '%h'
|
27
|
+
convert << 'info:'
|
28
|
+
end.to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/libav.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
#
|
2
|
+
# FFmpeg Abstraction
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'capturer'
|
6
|
+
require 'command'
|
7
|
+
require 'time_index'
|
8
|
+
|
9
|
+
module VCSRuby
|
10
|
+
class LibAV < Capturer
|
11
|
+
|
12
|
+
CODEC = 2
|
13
|
+
DIMENSION = 4
|
14
|
+
FPS = 6
|
15
|
+
|
16
|
+
def initialize video
|
17
|
+
@video = video
|
18
|
+
@avconv = Command.new :libav, 'avconv'
|
19
|
+
@avprobe = Command.new :libav, 'avprobe'
|
20
|
+
detect_version
|
21
|
+
end
|
22
|
+
|
23
|
+
def name
|
24
|
+
:libav
|
25
|
+
end
|
26
|
+
|
27
|
+
def available?
|
28
|
+
@avconv.available && @avprobe.available
|
29
|
+
end
|
30
|
+
|
31
|
+
def detect_version
|
32
|
+
info = @avconv.execute("-version")
|
33
|
+
match = /avconv ([\d|.|-|:]*)/.match(info)
|
34
|
+
@version = match[1]
|
35
|
+
end
|
36
|
+
|
37
|
+
def length
|
38
|
+
load_probe
|
39
|
+
match = /Duration: ([\d|:|.]*)/.match(@cache)
|
40
|
+
return TimeIndex.new match[1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def width
|
44
|
+
load_probe
|
45
|
+
@width
|
46
|
+
end
|
47
|
+
|
48
|
+
def height
|
49
|
+
load_probe
|
50
|
+
@height
|
51
|
+
end
|
52
|
+
|
53
|
+
def par
|
54
|
+
load_probe
|
55
|
+
@par
|
56
|
+
end
|
57
|
+
|
58
|
+
def dar
|
59
|
+
load_probe
|
60
|
+
@dar
|
61
|
+
end
|
62
|
+
|
63
|
+
def fps
|
64
|
+
load_probe
|
65
|
+
@fps
|
66
|
+
end
|
67
|
+
|
68
|
+
def video_codec
|
69
|
+
load_probe
|
70
|
+
|
71
|
+
@video_codec
|
72
|
+
end
|
73
|
+
|
74
|
+
def audio_codec
|
75
|
+
load_probe
|
76
|
+
|
77
|
+
@audio_codec
|
78
|
+
end
|
79
|
+
|
80
|
+
def grab time, image_path
|
81
|
+
@avconv.execute "-y -ss #{time.total_seconds} -i '#{@video}' -an -dframes 1 -vframes 1 -vcodec png -f rawvideo '#{image_path}'"
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
"LibAV #{@version}"
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
def load_probe
|
90
|
+
return if @cache
|
91
|
+
|
92
|
+
@cache = @avprobe.execute("'#{@video}'", "2>&1")
|
93
|
+
puts @cache if Tools.verbose?
|
94
|
+
|
95
|
+
parse_video_streams
|
96
|
+
parse_audio_streams
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_video_streams
|
100
|
+
video_stream = split_stream_line(is_stream?(@cache, /Video/).first)
|
101
|
+
|
102
|
+
dimensions = /(\d*)x(\d*) \[PAR (\d*:\d*) DAR (\d*:\d*)\]/.match(video_stream[DIMENSION])
|
103
|
+
|
104
|
+
if dimensions
|
105
|
+
@par = dimensions[3]
|
106
|
+
@dar = dimensions[4]
|
107
|
+
else
|
108
|
+
dimensions = /(\d*)x(\d*)/.match(video_stream[DIMENSION])
|
109
|
+
end
|
110
|
+
|
111
|
+
if dimensions
|
112
|
+
@width = dimensions[1].to_i
|
113
|
+
@height = dimensions[2].to_i
|
114
|
+
end
|
115
|
+
|
116
|
+
fps = /([\d|.]+) fps/.match(video_stream[FPS])
|
117
|
+
@fps = fps ? fps[1].to_f : 0.0
|
118
|
+
|
119
|
+
@video_codec = video_stream[CODEC]
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_audio_streams
|
123
|
+
audio_stream = split_stream_line(is_stream?(@cache, /Audio/).first)
|
124
|
+
|
125
|
+
@audio_codec = audio_stream[CODEC]
|
126
|
+
end
|
127
|
+
|
128
|
+
def is_stream? probe, regex
|
129
|
+
streams(probe).select{ |s| s =~ regex }
|
130
|
+
end
|
131
|
+
|
132
|
+
def streams probe
|
133
|
+
@cache.split(/\r?\n/).map(&:strip).select{|l| l.start_with? 'Stream' }
|
134
|
+
end
|
135
|
+
|
136
|
+
def split_stream_line line
|
137
|
+
parts = line.split(',')
|
138
|
+
stream = parts.shift
|
139
|
+
result = stream.split(':')
|
140
|
+
result += parts
|
141
|
+
return result.map(&:strip)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/mplayer.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# MPlayer Abstraction
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'command'
|
6
|
+
require 'capturer'
|
7
|
+
|
8
|
+
module VCSRuby
|
9
|
+
class MPlayer < Capturer
|
10
|
+
def initialize video
|
11
|
+
@video = video
|
12
|
+
@command = Command.new :mplayer, 'mplayer'
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
:mplayer
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/thumbnail.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
#
|
2
|
+
# Thumbnails from video
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'mini_magick'
|
6
|
+
|
7
|
+
module VCSRuby
|
8
|
+
class Thumbnail
|
9
|
+
attr_accessor :width, :height, :aspect
|
10
|
+
attr_accessor :image_path
|
11
|
+
attr_accessor :time
|
12
|
+
|
13
|
+
def initialize capper, video, configuration
|
14
|
+
@capper = capper
|
15
|
+
@video = video
|
16
|
+
@configuration = configuration
|
17
|
+
|
18
|
+
@filters = [method(:resize_filter), method(:timestamp_filter), method(:softshadow_filter)]
|
19
|
+
end
|
20
|
+
|
21
|
+
def capture
|
22
|
+
@capper.grab @time, @image_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def capture_and_evade interval
|
26
|
+
times = [TimeIndex.new] + @configuration.blank_alternatives
|
27
|
+
times.select! { |t| (t < interval / 2) and (t > interval / -2) }
|
28
|
+
times.map! { |t| @time + t }
|
29
|
+
|
30
|
+
times.each do |time|
|
31
|
+
@time = time
|
32
|
+
capture
|
33
|
+
break unless blank?
|
34
|
+
puts "Blank frame detected. => #{@time}" unless Tools::quiet?
|
35
|
+
puts "Giving up!" if time == times.last && !Tools::quiet?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def blank?
|
40
|
+
image = MiniMagick::Image.open @image_path
|
41
|
+
image.colorspace 'Gray'
|
42
|
+
mean = image['%[fx:image.mean]'].to_f
|
43
|
+
return mean < @configuration.blank_threshold
|
44
|
+
end
|
45
|
+
|
46
|
+
def apply_filters
|
47
|
+
MiniMagick::Tool::Convert.new do |convert|
|
48
|
+
convert.background 'Transparent'
|
49
|
+
convert.fill 'Transparent'
|
50
|
+
convert << @image_path
|
51
|
+
@filters.each do |filter|
|
52
|
+
filter.call(convert)
|
53
|
+
end
|
54
|
+
convert << @image_path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def resize_filter convert
|
60
|
+
convert.resize "#{width}x#{height}!"
|
61
|
+
end
|
62
|
+
|
63
|
+
def timestamp_filter convert
|
64
|
+
convert.stack do |box|
|
65
|
+
box.box @configuration.timestamp_background
|
66
|
+
box.fill @configuration.timestamp_color
|
67
|
+
box.pointsize @configuration.timestamp_font.size
|
68
|
+
box.gravity 'SouthEast'
|
69
|
+
box.font @configuration.timestamp_font.path
|
70
|
+
box.annotate('+10+10', " #{@time.to_timestamp} ")
|
71
|
+
end
|
72
|
+
convert.flatten
|
73
|
+
convert.gravity 'None'
|
74
|
+
end
|
75
|
+
|
76
|
+
def photoframe_filter convert
|
77
|
+
convert.bordercolor 'White'
|
78
|
+
convert.border 3
|
79
|
+
convert.bordercolor 'Grey60'
|
80
|
+
convert.border 1
|
81
|
+
end
|
82
|
+
|
83
|
+
def softshadow_filter convert
|
84
|
+
convert.stack do |box|
|
85
|
+
box.background 'Black'
|
86
|
+
box.clone.+
|
87
|
+
box.shadow '50x2+4+4'
|
88
|
+
box.background 'None'
|
89
|
+
end
|
90
|
+
convert.swap.+
|
91
|
+
convert.flatten
|
92
|
+
convert.trim
|
93
|
+
convert.repage.+
|
94
|
+
end
|
95
|
+
|
96
|
+
def polaroid_filter
|
97
|
+
end
|
98
|
+
|
99
|
+
def random_rotation_filter
|
100
|
+
end
|
101
|
+
|
102
|
+
def film_filter
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/time_index.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
#
|
2
|
+
# Time interval
|
3
|
+
#
|
4
|
+
|
5
|
+
module VCSRuby
|
6
|
+
class TimeIndex
|
7
|
+
include Comparable
|
8
|
+
attr_reader :total_seconds
|
9
|
+
|
10
|
+
def initialize time_index = ''
|
11
|
+
if time_index.instance_of? Float or time_index.instance_of? Fixnum
|
12
|
+
@total_seconds = time_index
|
13
|
+
else
|
14
|
+
@total_seconds = 0.0
|
15
|
+
@to_parse = time_index.strip
|
16
|
+
|
17
|
+
unless @to_parse.empty?
|
18
|
+
try_parse_ffmpeg_index
|
19
|
+
try_parse_vcs_index
|
20
|
+
try_parse_as_number
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def try_parse_ffmpeg_index
|
26
|
+
parts = @to_parse.split(':')
|
27
|
+
if parts.count == 3
|
28
|
+
@total_seconds += parts[0].to_i * 60 * 60
|
29
|
+
@total_seconds += parts[1].to_i * 60
|
30
|
+
@total_seconds += parts[2].to_f
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def try_parse_vcs_index
|
35
|
+
if @to_parse =~ /\d*m|\d*h|\d*s/
|
36
|
+
parts = @to_parse.split(/(\d*h)|(\d*m)|(\d*s)/).select{|e| !e.empty?}
|
37
|
+
parts.each do |part|
|
38
|
+
add_vcs_part part
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_vcs_part part
|
44
|
+
return @total_seconds += part.to_i * 60 * 60 if part.end_with? 'h'
|
45
|
+
return @total_seconds += part.to_i * 60 if part.end_with? 'm'
|
46
|
+
@total_seconds += part.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
def try_parse_as_number
|
50
|
+
temp = @to_parse.to_i
|
51
|
+
if temp.to_s == @to_parse
|
52
|
+
@total_seconds += temp
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def total_seconds
|
57
|
+
@total_seconds
|
58
|
+
end
|
59
|
+
|
60
|
+
def hours
|
61
|
+
(@total_seconds.abs / 3600).to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
def minutes
|
65
|
+
((@total_seconds.abs / 60) % 60).to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def seconds
|
69
|
+
@total_seconds.abs % 60
|
70
|
+
end
|
71
|
+
|
72
|
+
def + operand
|
73
|
+
if operand.instance_of? Fixnum
|
74
|
+
TimeIndex.new @total_seconds + operand
|
75
|
+
else
|
76
|
+
TimeIndex.new @total_seconds + operand.total_seconds
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def - operand
|
81
|
+
if operand.instance_of? Fixnum
|
82
|
+
TimeIndex.new @total_seconds - operand
|
83
|
+
else
|
84
|
+
TimeIndex.new @total_seconds - operand.total_seconds
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def * operand
|
89
|
+
TimeIndex.new total_seconds * operand
|
90
|
+
end
|
91
|
+
|
92
|
+
def / operand
|
93
|
+
if operand.instance_of? Fixnum
|
94
|
+
TimeIndex.new @total_seconds / operand
|
95
|
+
else
|
96
|
+
@total_seconds / operand.total_seconds
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def <=> operand
|
101
|
+
@total_seconds <=> operand.total_seconds
|
102
|
+
end
|
103
|
+
|
104
|
+
def sign
|
105
|
+
return '-' if @total_seconds < 0
|
106
|
+
''
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
"#{sign}#{hours}h#{"%02d" % minutes}m#{"%02d" % seconds}s"
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_timestamp
|
114
|
+
if hours == 0
|
115
|
+
"#{sign}#{"%02d" % minutes}:#{"%02d" % seconds}"
|
116
|
+
else
|
117
|
+
"#{sign}#{hours}:#{"%02d" % minutes}:#{"%02d" % seconds}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/tools.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#
|
2
|
+
# Dependencies
|
3
|
+
#
|
4
|
+
|
5
|
+
module VCSRuby
|
6
|
+
class Tools
|
7
|
+
def self.windows?
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.verbose= verbose
|
12
|
+
@verbose = verbose
|
13
|
+
@quiet = false if @verbose
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.verbose?
|
17
|
+
@verbose
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.quiet= quiet
|
21
|
+
@quiet = quiet
|
22
|
+
@verbose = false if @quiet
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.quiet?
|
26
|
+
@quiet
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.list_arguments arguments
|
30
|
+
arguments.map{ |argument| argument.to_s }.join(', ')
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.print_help optparse
|
34
|
+
puts optparse.summarize
|
35
|
+
exit 0
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.contact_sheet_with_options video, options
|
39
|
+
sheet = VCSRuby::ContactSheet.new video, options[:capturer]
|
40
|
+
sheet.format = options[:format] if options[:format]
|
41
|
+
sheet.title = options[:title] if options[:title]
|
42
|
+
sheet.signature = options[:signature] if options[:signature]
|
43
|
+
sheet.signature = nil if options[:no_signature]
|
44
|
+
|
45
|
+
if options[:rows] || options[:columns] || options[:interval]
|
46
|
+
sheet.initialize_geometry(options[:rows], options[:columns], options[:interval])
|
47
|
+
end
|
48
|
+
|
49
|
+
sheet.thumbnail_width = options[:width] if options[:width]
|
50
|
+
sheet.thumbnail_height = options[:height] if options[:height]
|
51
|
+
sheet.from = options[:from] if options[:from]
|
52
|
+
sheet.to = options[:to] if options[:to]
|
53
|
+
sheet.highlight = options[:highlight] if options[:highlight]
|
54
|
+
|
55
|
+
return sheet
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.to_human_size size
|
59
|
+
powers = { 'B' => 1 << 10, 'KiB' => 1 << 20, 'MiB' => 1 << 30, 'GiB' => 1 << 40, 'TiB' => 1 << 50 }
|
60
|
+
powers.each_pair do |prefix, power|
|
61
|
+
if size < power
|
62
|
+
return format('%.2f',size.to_f / (power >> 10)) + ' ' + prefix
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/vcs.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#
|
2
|
+
# Video Contact Sheet Ruby
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'command'
|
6
|
+
require 'configuration'
|
7
|
+
require 'contact_sheet'
|
8
|
+
require 'ffmpeg'
|
9
|
+
require 'libav'
|
10
|
+
require 'mplayer'
|
11
|
+
require 'thumbnail'
|
12
|
+
require 'time_index'
|
13
|
+
require 'tools'
|
14
|
+
require 'version'
|
data/lib/version.info
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.8.1
|
data/lib/version.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# Version of vcs.rb
|
3
|
+
#
|
4
|
+
|
5
|
+
module VCSRuby
|
6
|
+
def self.version_path
|
7
|
+
File.expand_path("version.info", File.dirname(__FILE__))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.read_version
|
11
|
+
File.open(version_path, &:readline)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.update_version
|
15
|
+
parts = File.open(version_path, &:readline).split('.').map(&:strip)
|
16
|
+
parts[2] = (parts[2].to_i + 1).to_s
|
17
|
+
File.open(version_path, 'w') {|f| f.write(parts.join('.')) }
|
18
|
+
|
19
|
+
$vcs_ruby_version
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
$vcs_ruby_version = Gem::Version.new(VCSRuby::read_version)
|
24
|
+
$vcs_ruby_name = 'Video Contact Sheet Ruby'
|
25
|
+
$vcs_ruby_short = 'vcr.rb'
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vcs_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thomas Bruderer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-04-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minimagick
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Creates a contact sheet of a video, usable as library or as a script.
|
31
|
+
Based on VCS *NIX
|
32
|
+
email: apophis@apophis.ch
|
33
|
+
executables:
|
34
|
+
- vcs.rb
|
35
|
+
extensions: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
files:
|
38
|
+
- lib/time_index.rb
|
39
|
+
- lib/font.rb
|
40
|
+
- lib/tools.rb
|
41
|
+
- lib/mplayer.rb
|
42
|
+
- lib/thumbnail.rb
|
43
|
+
- lib/contact_sheet.rb
|
44
|
+
- lib/ffmpeg.rb
|
45
|
+
- lib/version.rb
|
46
|
+
- lib/libav.rb
|
47
|
+
- lib/capturer.rb
|
48
|
+
- lib/version.info
|
49
|
+
- lib/vcs.rb
|
50
|
+
- lib/command.rb
|
51
|
+
- lib/configuration.rb
|
52
|
+
- lib/defaults.yml
|
53
|
+
- bin/vcs.rb
|
54
|
+
homepage: https://github.com/FreeApophis/vcs.rb
|
55
|
+
licenses:
|
56
|
+
- GPL3
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.8.6
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements:
|
74
|
+
- libav or ffmpeg or mplayer
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.8.23
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Generates contact sheets of videos
|
80
|
+
test_files: []
|