teslacam-merge 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +23 -0
- data/Rakefile +16 -0
- data/bin/teslacam-merge +22 -0
- data/lib/teslacam.rb +11 -0
- data/lib/teslacam/cli.rb +29 -0
- data/lib/teslacam/cli/config.rb +53 -0
- data/lib/teslacam/config.rb +36 -0
- data/lib/teslacam/filter.rb +226 -0
- data/lib/teslacam/model.rb +121 -0
- data/lib/teslacam/size.rb +5 -0
- data/license.txt +20 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c454a054f75c62785f06dad71f23a2f26ceacd75f261c8ffdbefc96f64942cd5
|
4
|
+
data.tar.gz: e7b7256e16be5868d489db58c6ae5c682075a5790b941bd5414fd8742fa62857
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a03cc0ccf4929faa055bbd3ae6d4a2d1475ca25f789b9a52f86b72c2c44de4136413f312cf0520e032761142a8dbde5cc8c79f6efc4a77d41a173e7dc0a1c9c
|
7
|
+
data.tar.gz: 21fd8321e18f5dd150e10a5b24e80eb4c8bfeabac6ca19e8f3bec9d2e474d38e479385c074a255a8e0ba6bbdff6a7bf27068febbac11f9c3befedafa71af28ae
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# teslacam-merge
|
2
|
+
|
3
|
+
Combine TeslaCam videos into a single output video. Allows you to set
|
4
|
+
the output video size, and add a title to the output video.
|
5
|
+
|
6
|
+
Example:
|
7
|
+
|
8
|
+
```
|
9
|
+
# combine given videos, set the title to "sample video", and write the
|
10
|
+
# result to the file "sentry-example.mp4"
|
11
|
+
teslacam-merge -t 'sample video' -s 320x240 -o sentry-example.mp4 \
|
12
|
+
2019-11-08_01-55-17-* 2019-11-08_01-48-14-{left,right}*.mp4
|
13
|
+
```
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Install `teslacam-merge` via [RubyGems][]:
|
18
|
+
|
19
|
+
```
|
20
|
+
gem install teslacam-merge
|
21
|
+
```
|
22
|
+
|
23
|
+
[rubygems]: https://rubygems.org/
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rdoc/task'
|
3
|
+
|
4
|
+
Rake::TestTask.new do |t|
|
5
|
+
t.libs << 'test'
|
6
|
+
end
|
7
|
+
|
8
|
+
RDoc::Task.new :docs do |t|
|
9
|
+
t.main = "lib/teslacam.rb"
|
10
|
+
t.rdoc_files.include('lib/*.rb')
|
11
|
+
t.rdoc_dir = 'docs'
|
12
|
+
# t.options << "--all"
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Run tests"
|
16
|
+
task default: :test
|
data/bin/teslacam-merge
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# teslacam-merge: Merge teslacam videos into one combined video.
|
5
|
+
#
|
6
|
+
# Example usage:
|
7
|
+
#
|
8
|
+
# # merge all 2019-11-07_22-22-26* videos into out.mp4
|
9
|
+
# teslacam-merge out.mp4 2019-11-07_22-22-26-*
|
10
|
+
#
|
11
|
+
# # merge all 2019-11-07_22-22-26* videos and 2019-11-07_22-23-26-*
|
12
|
+
# # videos into combined.mp4
|
13
|
+
# teslacam-merge combined.mp4 2019-11-07_22-23-26-* 2019-11-07_22-22-26-*
|
14
|
+
#
|
15
|
+
# notes:
|
16
|
+
# * src video size: 1280x960
|
17
|
+
# * ffmpeg command src: https://trac.ffmpeg.org/wiki/Create%20a%20mosaic%20out%20of%20several%20input%20videos
|
18
|
+
#
|
19
|
+
|
20
|
+
require_relative '../lib/teslacam'
|
21
|
+
|
22
|
+
::TeslaCam::CLI.run($0, ARGV) if __FILE__ == $0
|
data/lib/teslacam.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module TeslaCam
|
2
|
+
VERSION = '0.1.0'
|
3
|
+
|
4
|
+
LIB_DIR = File.join(__dir__, 'teslacam').freeze
|
5
|
+
|
6
|
+
autoload :CLI, File.join(LIB_DIR, 'cli.rb')
|
7
|
+
autoload :Config, File.join(LIB_DIR, 'config.rb')
|
8
|
+
autoload :Filter, File.join(LIB_DIR, 'filter.rb')
|
9
|
+
autoload :Model, File.join(LIB_DIR, 'model.rb')
|
10
|
+
autoload :Size, File.join(LIB_DIR, 'size.rb')
|
11
|
+
end
|
data/lib/teslacam/cli.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Command-line interface.
|
6
|
+
#
|
7
|
+
module TeslaCam::CLI
|
8
|
+
LIB_DIR = File.join(__dir__, 'cli').freeze
|
9
|
+
autoload :Config, File.join(LIB_DIR, 'config.rb')
|
10
|
+
|
11
|
+
#
|
12
|
+
# Run from command-line.
|
13
|
+
#
|
14
|
+
def self.run(app, args)
|
15
|
+
# get config from command-line, build model
|
16
|
+
config = ::TeslaCam::CLI::Config.new(app, args)
|
17
|
+
|
18
|
+
# create logger from config
|
19
|
+
log = ::Logger.new(config.quiet ? nil : STDERR)
|
20
|
+
|
21
|
+
# create model from config and log
|
22
|
+
model = ::TeslaCam::Model.new(config, log)
|
23
|
+
|
24
|
+
|
25
|
+
# exec command
|
26
|
+
log.debug { 'exec: %p' % [model.command] }
|
27
|
+
::Kernel.exec(*model.command)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Parse config from command-line arguments
|
5
|
+
#
|
6
|
+
class TeslaCam::CLI::Config < ::TeslaCam::Config
|
7
|
+
def initialize(app, args)
|
8
|
+
# initialize defaults
|
9
|
+
super()
|
10
|
+
|
11
|
+
@inputs = OptionParser.new do |o|
|
12
|
+
o.banner = "Usage: #{app} [options] <videos>"
|
13
|
+
o.separator ''
|
14
|
+
|
15
|
+
o.separator 'Options:'
|
16
|
+
o.on('-o', '--output [FILE]', String, 'Output file.') do |val|
|
17
|
+
@output = val
|
18
|
+
end
|
19
|
+
|
20
|
+
o.on('-s', '--size [SIZE]', String, 'Output size (WxH).') do |val|
|
21
|
+
md = val.match(/^(?<w>\d+)x(?<h>\d+)$/)
|
22
|
+
raise "invalid size: #{val}" unless md
|
23
|
+
@size = ::TeslaCam::Size.new(md[:w].to_i / 2, md[:h].to_i / 2)
|
24
|
+
end
|
25
|
+
|
26
|
+
o.on('--font-size [SIZE]', Integer, 'Font size.') do |val|
|
27
|
+
raise "invalid font size: #{val}" if val < 1
|
28
|
+
@font_size = val
|
29
|
+
end
|
30
|
+
|
31
|
+
o.on('--bg-color [COLOR]', Integer, 'Background color.') do |val|
|
32
|
+
raise "invalid font size: #{val}" if val < 1
|
33
|
+
@missing_color = val
|
34
|
+
end
|
35
|
+
|
36
|
+
o.on('-t', '--title [TITLE]', String, 'Video title.') do |val|
|
37
|
+
@title = val
|
38
|
+
end
|
39
|
+
|
40
|
+
o.on('-q', '--quiet', 'Silence ffmpeg output.') do
|
41
|
+
@quiet = true
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on_tail('-h', '--help', 'Show help.') do
|
45
|
+
puts o
|
46
|
+
exit 0
|
47
|
+
end
|
48
|
+
end.parse(args)
|
49
|
+
|
50
|
+
raise "missing input videos" unless @inputs.size > 0
|
51
|
+
raise "missing output" unless @output
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Parse command-line arguments into config
|
3
|
+
#
|
4
|
+
class TeslaCam::Config
|
5
|
+
attr :ffmpeg,
|
6
|
+
:quiet,
|
7
|
+
:output,
|
8
|
+
:inputs,
|
9
|
+
:size,
|
10
|
+
:font_size,
|
11
|
+
:missing_color,
|
12
|
+
:title
|
13
|
+
|
14
|
+
#
|
15
|
+
# Create a new Config instance and set defaults.
|
16
|
+
#
|
17
|
+
def initialize
|
18
|
+
# path to ffmpeg command
|
19
|
+
@ffmpeg = '/usr/bin/ffmpeg'
|
20
|
+
|
21
|
+
# make ffmpeg only show fatal errors
|
22
|
+
@quiet = false
|
23
|
+
|
24
|
+
# video title
|
25
|
+
@title = ''
|
26
|
+
|
27
|
+
# output size
|
28
|
+
@size = ::TeslaCam::Size.new(320, 240)
|
29
|
+
|
30
|
+
# font size
|
31
|
+
@font_size = 16
|
32
|
+
|
33
|
+
# background color for missing videos
|
34
|
+
@missing_color = 'black'
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class TeslaCam::Filter
|
4
|
+
def initialize(model)
|
5
|
+
config = model.config
|
6
|
+
num_times = model.times.size
|
7
|
+
|
8
|
+
# build filter string
|
9
|
+
@s = model.times.each_with_index.map { |time, i|
|
10
|
+
[
|
11
|
+
# get null source and video source nodes
|
12
|
+
sources(i, model, time),
|
13
|
+
|
14
|
+
# get overlay nodes
|
15
|
+
overlays(i, config),
|
16
|
+
|
17
|
+
# get text overlay nodes
|
18
|
+
texts(i, config, time, num_times),
|
19
|
+
]
|
20
|
+
}.flatten.concat(
|
21
|
+
# get concatenate node
|
22
|
+
concat_expr(num_times)
|
23
|
+
).join(';').freeze
|
24
|
+
|
25
|
+
# puts @s
|
26
|
+
# exit 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
@s
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
QUADS = {
|
36
|
+
tl: :front,
|
37
|
+
tr: :back,
|
38
|
+
bl: :left_repeater,
|
39
|
+
br: :right_repeater,
|
40
|
+
}
|
41
|
+
|
42
|
+
#
|
43
|
+
# Get video sources.
|
44
|
+
#
|
45
|
+
def sources(i, model, time)
|
46
|
+
# get missing color and quad size from config
|
47
|
+
color = model.config.missing_color
|
48
|
+
w = model.config.size.w
|
49
|
+
h = model.config.size.h
|
50
|
+
|
51
|
+
# build map of quad ID to argument number of corresponding video
|
52
|
+
# on command-line
|
53
|
+
lut = QUADS.keys.reduce({
|
54
|
+
# get command line argument offset for files in this set
|
55
|
+
ofs: i.times.reduce(0) do |r, j|
|
56
|
+
r + model.videos[model.times[j]].size
|
57
|
+
end,
|
58
|
+
|
59
|
+
args: []
|
60
|
+
}) do |r, id|
|
61
|
+
if model.videos[time][QUADS[id]]
|
62
|
+
r[:args] << { id: id, ofs: r[:ofs] }
|
63
|
+
r[:ofs] += 1
|
64
|
+
end
|
65
|
+
r
|
66
|
+
end[:args].each.with_object({}) do |row, r|
|
67
|
+
r[row[:id]] = row[:ofs]
|
68
|
+
end
|
69
|
+
|
70
|
+
# build null sources
|
71
|
+
[
|
72
|
+
"nullsrc=size=#{w*2}x#{h*2}:d=60, drawbox=c=#{color}:t=fill [v#{i}_bg]",
|
73
|
+
].concat((QUADS.keys - lut.keys).map { |id|
|
74
|
+
# missing video
|
75
|
+
"nullsrc=size=#{w}x#{h}:d=60 [v#{i}_#{id}]"
|
76
|
+
}).concat(lut.map { |id, ofs|
|
77
|
+
# command-line argument source
|
78
|
+
"[#{ofs}:v] setpts=PTS-STARTPTS, scale=#{w}x#{h} [v#{i}_#{id}]"
|
79
|
+
}).join(';')
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Ordered list of video overlays.
|
84
|
+
#
|
85
|
+
OVERLAYS = [{
|
86
|
+
srcs: %w{bg tl},
|
87
|
+
dst: 't0',
|
88
|
+
x: 0,
|
89
|
+
y: 0,
|
90
|
+
}, {
|
91
|
+
srcs: %w{t0 tr},
|
92
|
+
dst: 't1',
|
93
|
+
x: 1,
|
94
|
+
y: 0,
|
95
|
+
}, {
|
96
|
+
srcs: %w{t1 bl},
|
97
|
+
dst: 't2',
|
98
|
+
x: 0,
|
99
|
+
y: 1,
|
100
|
+
}, {
|
101
|
+
srcs: %w{t2 br},
|
102
|
+
dst: 't3',
|
103
|
+
x: 1,
|
104
|
+
y: 1,
|
105
|
+
}]
|
106
|
+
|
107
|
+
#
|
108
|
+
# Video overlay format string.
|
109
|
+
#
|
110
|
+
OVERLAY_FORMAT = '
|
111
|
+
[v%<i>d_%<src0>s][v%<i>d_%<src1>s]
|
112
|
+
overlay=shortest=0:x=%<x>d:y=%<y>d
|
113
|
+
[v%<i>d_%<dst>s]
|
114
|
+
'.gsub(/[\s\n]+/mx, ' ').strip.freeze
|
115
|
+
|
116
|
+
#
|
117
|
+
# Get video overlays.
|
118
|
+
#
|
119
|
+
def overlays(i, config)
|
120
|
+
w = config.size.w
|
121
|
+
h = config.size.h
|
122
|
+
|
123
|
+
OVERLAYS.map { |row|
|
124
|
+
OVERLAY_FORMAT % row.merge({
|
125
|
+
src0: row[:srcs].first,
|
126
|
+
src1: row[:srcs].last,
|
127
|
+
i: i,
|
128
|
+
x: row[:x] * w,
|
129
|
+
y: row[:y] * h,
|
130
|
+
})
|
131
|
+
}.join(';')
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Text overlays.
|
136
|
+
#
|
137
|
+
TEXTS = [{
|
138
|
+
text: 'front',
|
139
|
+
x: '4',
|
140
|
+
y: '3',
|
141
|
+
}, {
|
142
|
+
text: 'back',
|
143
|
+
x: '(w-text_w-4)',
|
144
|
+
y: '3',
|
145
|
+
}, {
|
146
|
+
text: 'left',
|
147
|
+
x: '4',
|
148
|
+
y: '(h-text_h-3)',
|
149
|
+
}, {
|
150
|
+
text: 'right',
|
151
|
+
x: '(w-text_w-4)',
|
152
|
+
y: '(h-text_h-3)',
|
153
|
+
}, {
|
154
|
+
text: '%%{pts\\\\:localtime\\\\:%<ts>i}',
|
155
|
+
x: '(w-text_w)/2',
|
156
|
+
y: '(h-text_h-5)',
|
157
|
+
}, {
|
158
|
+
text: '%<title>s',
|
159
|
+
x: '(w-text_w)/2',
|
160
|
+
y: '3',
|
161
|
+
}]
|
162
|
+
|
163
|
+
#
|
164
|
+
# Get text overlays
|
165
|
+
#
|
166
|
+
def texts(i, config, time, num_times)
|
167
|
+
# get font
|
168
|
+
font = get_font(config)
|
169
|
+
|
170
|
+
# build text args
|
171
|
+
text_args = {
|
172
|
+
# timestamp offset
|
173
|
+
ts: Time.parse(time).to_i,
|
174
|
+
|
175
|
+
# video title
|
176
|
+
title: config.title, # FIXME: need to escape this
|
177
|
+
}
|
178
|
+
|
179
|
+
# build and return result
|
180
|
+
'[v%<i>d_t3] %<texts>s %<sink>s' % {
|
181
|
+
i: i,
|
182
|
+
|
183
|
+
texts: TEXTS.map { |row|
|
184
|
+
'drawtext=text=%<text>s:x=%<x>s:y=%<y>s:%<font>s' % row.merge({
|
185
|
+
text: row[:text] % text_args,
|
186
|
+
font: font,
|
187
|
+
})
|
188
|
+
}.join(', '),
|
189
|
+
|
190
|
+
sink: (num_times > 1) ? "[v#{i}]" : ''
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
#
|
195
|
+
# Get font config
|
196
|
+
#
|
197
|
+
def get_font(config)
|
198
|
+
[
|
199
|
+
'fontcolor=white@0.8',
|
200
|
+
"fontsize=#{config.font_size}",
|
201
|
+
|
202
|
+
# 'shadowcolor=black@0.5',
|
203
|
+
# 'shadowx=1',
|
204
|
+
# 'shadowy=1',
|
205
|
+
|
206
|
+
# 'box=1',
|
207
|
+
# 'boxborderw=2',
|
208
|
+
# 'boxcolor=black@0.5',
|
209
|
+
|
210
|
+
'borderw=1',
|
211
|
+
'bordercolor=black@0.5',
|
212
|
+
].join(':')
|
213
|
+
end
|
214
|
+
|
215
|
+
#
|
216
|
+
# Get concat statement.
|
217
|
+
#
|
218
|
+
def concat_expr(num_times)
|
219
|
+
(num_times > 1) ? [
|
220
|
+
'%s concat=n=%d' % [
|
221
|
+
num_times.times.map { |i| "[v#{i}]" }.join(''),
|
222
|
+
num_times,
|
223
|
+
],
|
224
|
+
] : []
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
class TeslaCam::Model
|
2
|
+
attr :config,
|
3
|
+
:videos,
|
4
|
+
:times,
|
5
|
+
:paths,
|
6
|
+
:filter,
|
7
|
+
:command
|
8
|
+
|
9
|
+
#
|
10
|
+
# Format string for ISO-8601 timestamps.
|
11
|
+
#
|
12
|
+
TIME_FORMAT = '%<year>s-%<month>s-%<day>sT%<hour>s:%<min>s:%<sec>s'
|
13
|
+
|
14
|
+
#
|
15
|
+
# Regular expression to extract relevant data from video file names.
|
16
|
+
#
|
17
|
+
VIDEO_PATH_RE = %r{^
|
18
|
+
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})_
|
19
|
+
(?<hour>\d{2})-(?<min>\d{2})-(?<sec>\d{2})-
|
20
|
+
(?<name>[a-z_]+)\.mp4
|
21
|
+
$}mx
|
22
|
+
|
23
|
+
#
|
24
|
+
# list of camera symbols
|
25
|
+
#
|
26
|
+
CAMS = %i{front back left_repeater right_repeater}
|
27
|
+
|
28
|
+
def initialize(config, log)
|
29
|
+
# cache config
|
30
|
+
@config = config
|
31
|
+
@log = log
|
32
|
+
|
33
|
+
# extract video sets from input file names
|
34
|
+
@videos = parse_inputs(config.inputs)
|
35
|
+
@times = @videos.keys.sort
|
36
|
+
@paths = get_paths(@times, @videos)
|
37
|
+
@filter = TeslaCam::Filter.new(self)
|
38
|
+
@command = get_command(config, @paths, @filter)
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Are there multiple video sets?
|
43
|
+
#
|
44
|
+
def multiple_video_sets?
|
45
|
+
@videos.keys.size > 1
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
#
|
51
|
+
# extract video sets from input file names
|
52
|
+
#
|
53
|
+
def parse_inputs(inputs)
|
54
|
+
# extract video sets from input file names
|
55
|
+
videos = inputs.each.with_object(Hash.new { |h, k| h[k] = {} }) do |path, r|
|
56
|
+
if md = ::File.basename(path).match(VIDEO_PATH_RE)
|
57
|
+
# build data
|
58
|
+
data = (md.names.each.with_object({}) { |s, mr|
|
59
|
+
i = s.intern
|
60
|
+
mr[i] = md[i]
|
61
|
+
}).merge({
|
62
|
+
path: path,
|
63
|
+
})
|
64
|
+
|
65
|
+
# build key
|
66
|
+
key = TIME_FORMAT % data
|
67
|
+
r[key][md[:name].intern] = data
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# check for missing videos in each set
|
72
|
+
videos.each do |time, videos|
|
73
|
+
missing = CAMS - videos.keys
|
74
|
+
if missing.size > 0
|
75
|
+
# some videos missing, raise warning
|
76
|
+
@log.warn { "missing videos in #{time}: #{missing * ', '}" }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# return videos
|
81
|
+
videos
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Build ordered list of video input paths.
|
86
|
+
#
|
87
|
+
def get_paths(times, videos)
|
88
|
+
times.reduce([]) do |r, time|
|
89
|
+
CAMS.select { |cam|
|
90
|
+
videos[time].key?(cam)
|
91
|
+
}.reduce(r) do |r, cam|
|
92
|
+
r << videos[time][cam][:path]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
#
|
98
|
+
# build ffmpeg command
|
99
|
+
#
|
100
|
+
def get_command(config, paths, filter)
|
101
|
+
[
|
102
|
+
config.ffmpeg,
|
103
|
+
|
104
|
+
# hide ffmpeg banner
|
105
|
+
'-hide_banner',
|
106
|
+
|
107
|
+
# set ffmpeg log level
|
108
|
+
'-loglevel', (config.quiet ? 'fatal' : 'info'),
|
109
|
+
|
110
|
+
# sorted list of videos (in order of CAMS, see above)
|
111
|
+
*(paths.map { |path| ['-i', path] }.flatten),
|
112
|
+
|
113
|
+
# filter command
|
114
|
+
'-filter_complex', filter.to_s,
|
115
|
+
'-c:v', 'libx264',
|
116
|
+
|
117
|
+
# output path
|
118
|
+
config.output,
|
119
|
+
]
|
120
|
+
end
|
121
|
+
end
|
data/license.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Paul Duncan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included
|
12
|
+
in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
15
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: teslacam-merge
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Duncan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: "\n Combine TeslaCam videos into a single output video. Allows you
|
14
|
+
to\n set the output video size, and add a title to the output video.\n "
|
15
|
+
email: pabs@pablotron.org
|
16
|
+
executables:
|
17
|
+
- teslacam-merge
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- bin/teslacam-merge
|
24
|
+
- lib/teslacam.rb
|
25
|
+
- lib/teslacam/cli.rb
|
26
|
+
- lib/teslacam/cli/config.rb
|
27
|
+
- lib/teslacam/config.rb
|
28
|
+
- lib/teslacam/filter.rb
|
29
|
+
- lib/teslacam/model.rb
|
30
|
+
- lib/teslacam/size.rb
|
31
|
+
- license.txt
|
32
|
+
homepage: https://github.com/pablotron/teslacam-merge
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata:
|
36
|
+
bug_tracker_uri: https://github.com/pablotron/teslacam-merge/issues
|
37
|
+
documentation_uri: https://pablotron.github.io/teslacam-merge/
|
38
|
+
homepage_uri: https://github.com/pablotron/teslacam-merge
|
39
|
+
source_code_uri: https://github.com/pablotron/teslacam-merge
|
40
|
+
wiki_uri: https://github.com/pablotron/teslacam-merge/wiki
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 2.7.6.2
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Combine TeslaCam videos into a single output video.
|
61
|
+
test_files: []
|