vcs_ruby 0.8.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/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: []
|