teslacam-merge 0.1.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.
- 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: []
|