ffmprb 0.12.1 → 0.12.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +10 -10
- data/README.md +7 -2
- data/coverage/index.html +2541 -2123
- data/exp/present/Gemfile.lock +2 -2
- data/exp/present/exp/present.rb +8 -2
- data/exp/youtubby/Gemfile +1 -3
- data/exp/youtubby/Gemfile.lock +3 -5
- data/exp/youtubby/exp/gop-raw-cut-rcc-join-you-HD60.rb +356 -0
- data/exp/youtubby/exp/gop-raw-cut-you-HD60 +3 -1
- data/exp/youtubby/exp/gop-raw-cut-you-HD60.rb +191 -63
- data/exp/youtubby/google_youtube.rb +3 -3
- data/ffmprb.gemspec +2 -0
- data/lib/defaults.rb +1 -1
- data/lib/ffmprb/file/sample.rb +15 -7
- data/lib/ffmprb/file.rb +13 -5
- data/lib/ffmprb/filter.rb +19 -7
- data/lib/ffmprb/process/input/chain_base.rb +5 -0
- data/lib/ffmprb/process/input/cut.rb +0 -4
- data/lib/ffmprb/process/input/paced.rb +3 -1
- data/lib/ffmprb/process/input.rb +4 -4
- data/lib/ffmprb/process/output.rb +2 -11
- data/lib/ffmprb/process.rb +22 -13
- data/lib/ffmprb/util/thread.rb +6 -3
- data/lib/ffmprb/util.rb +8 -6
- data/lib/ffmprb/version.rb +1 -1
- metadata +4 -3
@@ -1,13 +1,104 @@
|
|
1
|
+
|
2
|
+
# The general idea of (a video production pipeline for) "vlogging":
|
3
|
+
# -1. think of something nice to shoot
|
4
|
+
# - maybe even write down a scenario and produce "the picture"
|
5
|
+
# 0. shoot the damn footage with your cam/mic/what-have-you
|
6
|
+
# - press Record on your GoPro (video stored on device)
|
7
|
+
# 1. upload the raw media files to a (cloud) storage
|
8
|
+
# - GoPro media storage subscription (video files accessible at)
|
9
|
+
# 2. take note of, sort and clean the raw footage up manually
|
10
|
+
# - cut and concatenate the media files into cohesive "scenes" (ffmprb script)
|
11
|
+
# 3. upload the HD renders of the raw scenes
|
12
|
+
# - to a "cache" storage (ghlr media), plus, for the public access (YouTube)
|
13
|
+
# 4. compose a video from the raw scene cuts and additional media
|
14
|
+
# - create a (ffmprb) montage script using the media
|
15
|
+
# 5. upload and premiere the video piece versions (final cut, general public)
|
16
|
+
# - to "ghlr media sharing" (fut.), plus, for public "discovery" (YouTube)
|
17
|
+
# 6. profit?
|
18
|
+
|
19
|
+
# # empty (micro) case is raw publish
|
20
|
+
|
21
|
+
# NOTE on simprender:
|
22
|
+
# a title-back-summary effect
|
23
|
+
# XXX
|
24
|
+
# -16(2) -8(2) -4(2) ^0 2(2), -4(1) [1]
|
25
|
+
# crop
|
26
|
+
# ?soundtrack?
|
27
|
+
# cut(from 24, to: 56).pace(16).reverse
|
28
|
+
# cut(from 8, to: 24).pace(8).reverse
|
29
|
+
# cut(from 0, to: 8).pace(4).reverse
|
30
|
+
# cut(from: 0, to: 4).pace(2)
|
31
|
+
# cut(from: 0, to: 4).pace(4).reverse
|
32
|
+
#
|
33
|
+
# 2.0
|
34
|
+
# crop(0.48).cut(from: M).pace(m).reverse
|
35
|
+
# ...
|
36
|
+
# crop(0.2).cut(from: 12, to: 28).pace(16).reverse
|
37
|
+
# crop(0.18).cut(from: 4, to: 12).pace(8).reverse
|
38
|
+
# crop(0.15).cut(from: 0, to: 4).pace(4).reverse
|
39
|
+
# crop(0.1).cut(from: 0, to: 4).pace(2)
|
40
|
+
# crop(0.05).cut(from: 0, to: 4).pace(4).reverse
|
41
|
+
#
|
42
|
+
# 3.0 (aka 1.11)
|
43
|
+
# crop(0.49).cut(from: M).pace(m).reverse
|
44
|
+
# ...
|
45
|
+
# crop(0.2).cut(from: 12, to: 28).pace(16).reverse
|
46
|
+
# crop(0.25).cut(from: 4, to: 12).pace(8).reverse
|
47
|
+
# crop(0.14).cut(from: 0, to: 4).pace(4).reverse
|
48
|
+
|
49
|
+
# NOTE looking good?
|
50
|
+
REV_CUT_CROP_EXP_BASE = 1.333
|
51
|
+
|
52
|
+
# NOTE unreasonable to exhaust
|
53
|
+
REV_CUT_CROP_EXP_PRE_CUTS =
|
54
|
+
(2..64).reduce [] do |a, k|
|
55
|
+
if (3.6..3600.0).include? (int = REV_CUT_CROP_EXP_BASE**k)
|
56
|
+
a << (a[-1] || 0) + int
|
57
|
+
else
|
58
|
+
a
|
59
|
+
end
|
60
|
+
end.reverse
|
61
|
+
|
62
|
+
# NOTE reasonable to look good
|
63
|
+
REV_CUT_CROP_LENGTH_MIN_S = REV_CUT_CROP_EXP_PRE_CUTS[-4]
|
64
|
+
|
65
|
+
# XXX Can also go hard on with blending to complement the rewind effect...
|
66
|
+
|
67
|
+
def rev_cut_crop_seq(length, inp)
|
68
|
+
fail "supply a block that receives the next reel and returns the next input" unless
|
69
|
+
block_given?
|
70
|
+
|
71
|
+
return unless
|
72
|
+
length > REV_CUT_CROP_LENGTH_MIN_S
|
73
|
+
|
74
|
+
REV_CUT_CROP_EXP_PRE_CUTS.each_with_index do |k, i|
|
75
|
+
next if
|
76
|
+
(n = REV_CUT_CROP_EXP_PRE_CUTS[i + 1] || 0) > length
|
77
|
+
cut = {from: n}
|
78
|
+
cut[:to] = k unless
|
79
|
+
k > length
|
80
|
+
pace = REV_CUT_CROP_EXP_BASE**(REV_CUT_CROP_EXP_PRE_CUTS.length - i + 1)
|
81
|
+
# XXX Math.log(1 + (REV_CUT_CROP_EXP_PRE_CUTS.length - i + 2) / 5.0) / 4 # NOTE magick!
|
82
|
+
crop = 0.5*(1 - Math.sqrt(1/pace)) # NOTE bitrate! (x2)
|
83
|
+
# inp = yield(inp.crop(crop).cut(cut).pace(pace).reverse)
|
84
|
+
yield inp.crop(crop).cut(cut).pace(pace).reverse
|
85
|
+
end
|
86
|
+
# XXX
|
87
|
+
# inp = yield(inp.crop(0.1).cut(from: 0, to: 4).pace(2))
|
88
|
+
# inp = yield(inp.crop(0.05).cut(from: 0, to: 4).pace(4).reverse)
|
89
|
+
end
|
90
|
+
|
1
91
|
channel = ARGV.shift || 'default'
|
2
92
|
|
93
|
+
# XXX too complicated? maybe 3 scripts instead?
|
3
94
|
abort "USAGE: gop-raw-cut-you-HD60.rb [CHANNEL]" unless
|
4
95
|
ARGV.empty?
|
5
96
|
|
6
97
|
MEDIA_DIR = ENV['MEDIA_DIR'] or
|
7
98
|
abort "MEDIA_DIR needed"
|
8
99
|
|
9
|
-
|
10
|
-
title = "Topublish uploaded at #{
|
100
|
+
now_time = Time.now
|
101
|
+
title = "XXX Topublish (uploaded at #{now_time.strftime '%y-%m-%d-%H-%M'})"
|
11
102
|
|
12
103
|
require 'cgi'
|
13
104
|
|
@@ -26,14 +117,16 @@ end
|
|
26
117
|
require 'fileutils'
|
27
118
|
|
28
119
|
require 'ffmprb'
|
29
|
-
|
120
|
+
Ffmprb.debug = true # XXX
|
30
121
|
Ffmprb::Util::Thread.timeout = 150
|
31
122
|
|
32
123
|
int_video_opt = {resolution: Ffmprb::HD_4K, fps: 60}
|
33
|
-
|
34
|
-
|
124
|
+
raw_video_opt = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264 -crf 15'} # XXX -preset superfast
|
125
|
+
fin_video_opt = {resolution: Ffmprb::HD_1080p, fps: 60, encoder: 'libx265 -crf 19'} # XXX 21 -preset veryslow
|
126
|
+
YOU_VIDEO_OPT = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264 -crf 17'} # XXX 15 -preset superfast
|
35
127
|
|
36
|
-
|
128
|
+
GOP_FILE_PREFIX = 'GX'
|
129
|
+
GOP_MP4_RE = /\b(#{GOP_FILE_PREFIX}(\d\d)(\d\d\d\d)\.MP4)\b/i
|
37
130
|
GOP_ZIP_URL_RE = %r[/zip/]i
|
38
131
|
|
39
132
|
|
@@ -43,10 +136,11 @@ def dura_to_sec(dura_str)
|
|
43
136
|
end
|
44
137
|
end
|
45
138
|
|
46
|
-
def
|
47
|
-
File.join MEDIA_DIR, "#{name}.mp4"
|
139
|
+
def date_media_path(date, name)
|
140
|
+
File.join MEDIA_DIR, '%04d' % date.year, '%02d' % date.month, '%02d' % date.day, "#{name}.mp4"
|
48
141
|
end
|
49
142
|
|
143
|
+
raw_time = nil
|
50
144
|
|
51
145
|
FileUtils.mkdir_p (tmp_dir = File.join(MEDIA_DIR, 'gop-raw-cut-you-tmp'))
|
52
146
|
begin
|
@@ -56,12 +150,13 @@ begin
|
|
56
150
|
warn "\nEnter lines containing GoP media D/L URLs and cut times:\n\n"
|
57
151
|
|
58
152
|
av_src_cuts = []
|
153
|
+
int_av_path = nil
|
59
154
|
dl_q = Queue.new
|
60
155
|
|
61
|
-
shots = []
|
62
156
|
fetcher = Thread.new do
|
63
157
|
while (url, cuts = dl_q.deq)
|
64
158
|
srcs = []
|
159
|
+
shot = nil
|
65
160
|
while srcs.empty? # NOTE sometimes (zip) D/L silently fails, see below
|
66
161
|
name =
|
67
162
|
case CGI.unescape url
|
@@ -80,16 +175,28 @@ begin
|
|
80
175
|
zip_lines.each do |line|
|
81
176
|
if line =~ GOP_MP4_RE
|
82
177
|
srcs << $1
|
83
|
-
|
178
|
+
shot = $3
|
84
179
|
end
|
85
180
|
end
|
86
181
|
end
|
87
182
|
File.delete name
|
88
183
|
else
|
89
184
|
srcs << name
|
90
|
-
|
185
|
+
shot = $3
|
91
186
|
end
|
92
187
|
end
|
188
|
+
|
189
|
+
# NOTE assuming srcs is not empty, for the first src
|
190
|
+
unless raw_time
|
191
|
+
raw_time =
|
192
|
+
Ffmprb::File.access(srcs[0]).creation_time ||
|
193
|
+
now_time # NOTE well...
|
194
|
+
int_av_path = date_media_path(raw_time, "#{GOP_FILE_PREFIX}-#{shot}")
|
195
|
+
# XXX
|
196
|
+
# break if
|
197
|
+
# (Ffmprb::File.access(int_av_path).length rescue 0) > 0
|
198
|
+
end
|
199
|
+
|
93
200
|
av_src_cuts << [
|
94
201
|
srcs.sort do |a, b|
|
95
202
|
a_m = GOP_MP4_RE.match(a)
|
@@ -103,6 +210,9 @@ begin
|
|
103
210
|
cuts
|
104
211
|
]
|
105
212
|
end
|
213
|
+
|
214
|
+
abort "ERROR no inputs given" unless
|
215
|
+
int_av_path
|
106
216
|
end
|
107
217
|
|
108
218
|
while (url_cut = gets)
|
@@ -123,20 +233,17 @@ begin
|
|
123
233
|
warn "\nFetching those files..."
|
124
234
|
fetcher.join
|
125
235
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
out_name = "GX-#{shots.uniq.join '-'}-#{time_s}"
|
130
|
-
you_out_path = "_you_#{out_name}.mp4"
|
131
|
-
warn "\nCut-catting to out (#{out_name}) paths + you..."
|
236
|
+
out_path = date_media_path(now_time, "#{GOP_FILE_PREFIX}-#{raw_time.strftime '%y-%m-%d-%H-%M'}-simprender")
|
237
|
+
you_out_path = "_you_toul.mp4"
|
238
|
+
warn "\nCut-catting to cache (#{int_av_path})"
|
132
239
|
|
133
240
|
pipe_cut_threads = av_src_cuts.map do |srcs, cuts|
|
134
241
|
[
|
135
|
-
(
|
242
|
+
(tmp_av_stream = Ffmprb::File.temp_fifo('.flv')),
|
136
243
|
cuts.each_slice(2).map { |from, to| {from: from, to: to} },
|
137
244
|
Thread.new do
|
138
245
|
Ffmprb.process do
|
139
|
-
output
|
246
|
+
output tmp_av_stream, video: int_video_opt do
|
140
247
|
srcs.each do |src|
|
141
248
|
roll input src
|
142
249
|
end
|
@@ -146,47 +253,80 @@ begin
|
|
146
253
|
]
|
147
254
|
end
|
148
255
|
|
149
|
-
Ffmprb::File.temp_fifo('.flv') do |
|
256
|
+
Ffmprb::File.temp_fifo('.flv') do |av_stream|
|
150
257
|
thr = Thread.new do
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
258
|
+
unless pipe_cut_threads.empty?
|
259
|
+
FileUtils.mkdir_p File.dirname int_av_path
|
260
|
+
Ffmprb.process do
|
261
|
+
inp_cut_opts =
|
262
|
+
pipe_cut_threads.map do |tmp_av_stream, cut_opts, _|
|
263
|
+
(cut_opts.empty?? [{}] : cut_opts).map do |cut_opt|
|
264
|
+
[input(tmp_av_stream), cut_opt]
|
265
|
+
end
|
266
|
+
end.reduce :+
|
267
|
+
# XXX output int_av_path, video: raw_video_opt do
|
268
|
+
output av_stream, video: raw_video_opt do
|
269
|
+
inp_cut_opts.each do |inp, cut_opt|
|
270
|
+
roll inp.cut cut_opt
|
271
|
+
end
|
161
272
|
end
|
162
273
|
end
|
163
|
-
# [Rational(1)/8, Rational(1)/4, Rational(1)/2, 1, 2, 4, 8].each do |r| # XXX
|
164
|
-
# output out_path("#{out_name}x#{r.to_f}"), video: fin_video_opt do
|
165
|
-
# inp_cut_opts.each do |inp, cut_opt|
|
166
|
-
# roll inp.cut(cut_opt).pace r
|
167
|
-
# end
|
168
|
-
# end
|
169
|
-
# end
|
170
274
|
|
171
|
-
# XXX
|
172
|
-
#
|
173
|
-
#
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
275
|
+
# XXX this is flawed:
|
276
|
+
# the simple threads fail because of oom docker signals (9)
|
277
|
+
# (not because of broken pipes)
|
278
|
+
pipe_cut_threads.each do |tmp_av_stream, _, thr|
|
279
|
+
begin
|
280
|
+
thr.join
|
281
|
+
rescue
|
282
|
+
warn "WARN errors-a-happening: #{$!}"
|
283
|
+
end
|
284
|
+
tmp_av_stream.unlink
|
285
|
+
end
|
182
286
|
end
|
287
|
+
|
288
|
+
# XXX
|
289
|
+
# warn "\nComposing and rendering to out (#{out_path}) + you..."
|
290
|
+
# Ffmprb.process do
|
291
|
+
# output av_stream, video: int_video_opt do
|
292
|
+
# # XXX roll in1.crop(0.25).cut(from: Ffmprb::File.access(int_av_path).length - 512).pace(16).reverse
|
293
|
+
|
294
|
+
# rev_cut_crop_seq(Ffmprb::File.access(int_av_path).length, input(int_av_path)) do |reel|
|
295
|
+
# roll reel
|
296
|
+
# end
|
297
|
+
# roll input(int_av_path)
|
298
|
+
# end
|
299
|
+
# end
|
300
|
+
# [Rational(1)/8, Rational(1)/4, Rational(1)/2, 1, 2, 4, 8].each do |r| # XXX
|
301
|
+
# output out_path("#{out_name}x#{r.to_f}"), video: fin_video_opt do
|
302
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
303
|
+
# roll inp.cut(cut_opt).pace r
|
304
|
+
# end
|
305
|
+
# end
|
306
|
+
# end
|
307
|
+
|
308
|
+
# XXX
|
309
|
+
# output out_path("#{out_name}x-8"), video: fin_video_opt do
|
310
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
311
|
+
# roll inp.cut(cut_opt).reverse.pace 8
|
312
|
+
# end
|
313
|
+
# end
|
314
|
+
# output out_path("#{out_name}x8-"), video: fin_video_opt do
|
315
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
316
|
+
# roll inp.cut(cut_opt).pace(8).reverse
|
317
|
+
# end
|
318
|
+
# end
|
183
319
|
end
|
320
|
+
# XXX bad romance: abort from any thread
|
184
321
|
begin
|
322
|
+
FileUtils.mkdir_p File.dirname out_path
|
185
323
|
Ffmprb.process do
|
186
|
-
in1 = input(
|
324
|
+
in1 = input(av_stream)
|
187
325
|
output you_out_path, video: YOU_VIDEO_OPT do
|
188
|
-
roll in1
|
189
|
-
|
326
|
+
roll in1
|
327
|
+
end
|
328
|
+
output out_path, video: fin_video_opt do
|
329
|
+
roll in1 # XXX .pp
|
190
330
|
end
|
191
331
|
end
|
192
332
|
ensure
|
@@ -195,18 +335,6 @@ begin
|
|
195
335
|
end
|
196
336
|
|
197
337
|
|
198
|
-
# XXX this is flawed:
|
199
|
-
# the simple threads fail because of oom docker signals (9)
|
200
|
-
# (not because of broken pipes)
|
201
|
-
pipe_cut_threads.each do |av_pipe, _, thr|
|
202
|
-
begin
|
203
|
-
thr.join
|
204
|
-
rescue
|
205
|
-
warn "WARN errors-a-happening: #{$!}"
|
206
|
-
end
|
207
|
-
av_pipe.unlink
|
208
|
-
end
|
209
|
-
|
210
338
|
metadata = {
|
211
339
|
snippet: {
|
212
340
|
title: title
|
@@ -22,9 +22,9 @@ def google_youtube(cb_uri, user_id, &blk)
|
|
22
22
|
Google::Apis::YoutubeV3::AUTH_YOUTUBE,
|
23
23
|
Google::Auth::Stores::FileTokenStore.new(file: GOOGLE_CREDENTIAL_STORE)
|
24
24
|
)
|
25
|
-
youtube.client_options.
|
26
|
-
youtube.client_options.
|
27
|
-
youtube.client_options.
|
25
|
+
youtube.client_options.send_timeout =
|
26
|
+
youtube.client_options.open_timeout =
|
27
|
+
youtube.client_options.read_timeout = YOUTUBE_TIMEOUT
|
28
28
|
youtube.authorization =
|
29
29
|
if (credentials = authorizer.get_credentials(user_id))
|
30
30
|
credentials
|
data/ffmprb.gemspec
CHANGED
@@ -47,6 +47,8 @@ Gem::Specification.new do |spec|
|
|
47
47
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
48
48
|
spec.require_paths = ['lib']
|
49
49
|
|
50
|
+
spec.required_ruby_version = '>= 2.6' # TODO check 3.X
|
51
|
+
|
50
52
|
# NOTE I'm not happy with this dependency, and there's nothing crossplatform (= for windoze too) at the moment
|
51
53
|
spec.add_dependency 'mkfifo', '~> 0.1.1'
|
52
54
|
# NOTE make it into an optional dependency? Nah for now
|
data/lib/defaults.rb
CHANGED
data/lib/ffmprb/file/sample.rb
CHANGED
@@ -2,8 +2,11 @@ module Ffmprb
|
|
2
2
|
|
3
3
|
class File
|
4
4
|
|
5
|
+
AUDIO_SAMPLE_MIN = 0.5
|
6
|
+
|
5
7
|
def sample(
|
6
8
|
at: 0.01,
|
9
|
+
duration: 0,
|
7
10
|
video: true,
|
8
11
|
audio: true,
|
9
12
|
&blk
|
@@ -11,15 +14,22 @@ module Ffmprb
|
|
11
14
|
audio = File.temp('.wav') if audio == true
|
12
15
|
video = File.temp('.png') if video == true
|
13
16
|
|
14
|
-
Ffmprb.logger.debug{"Snap shooting files, video
|
17
|
+
Ffmprb.logger.debug{"Snap shooting files, video: #{video && video.path}, audio: #{audio && audio.path}"}
|
15
18
|
|
16
|
-
fail Error, "Incorrect output extname (must be image)" unless
|
17
|
-
|
18
|
-
fail Error, "
|
19
|
+
fail Error, "Incorrect output extname (must be image)" unless
|
20
|
+
!video || video.channel?(:video) && !video.channel?(:audio)
|
21
|
+
fail Error, "Incorrect audio extname (must be sound)" unless
|
22
|
+
!audio || audio.channel?(:audio) && !audio.channel?(:video)
|
23
|
+
fail Error, "Can sample either video OR audio UNLESS a block is given" unless
|
24
|
+
block_given? || !!audio != !!video
|
25
|
+
fail Error, "Can sample video just for 0 sec (an image snapshot)" unless
|
26
|
+
!video || duration == 0
|
19
27
|
|
20
28
|
cmd = %W[-i #{path}]
|
21
29
|
cmd.concat %W[-deinterlace -an -ss #{at} -vframes 1 #{video.path}] if video
|
22
|
-
|
30
|
+
audio_duration = [AUDIO_SAMPLE_MIN, duration].max
|
31
|
+
audio_at = [0, at - audio_duration / 2].max
|
32
|
+
cmd.concat %W[-vn -ss #{audio_at} -t #{audio_duration} #{audio.path}] if audio
|
23
33
|
Util.ffmpeg *cmd
|
24
34
|
|
25
35
|
return video || audio unless block_given?
|
@@ -42,7 +52,5 @@ module Ffmprb
|
|
42
52
|
def sample_audio(*audio, at: 0.01, &blk)
|
43
53
|
sample at: at, video: false, audio: (audio.first || true), &blk
|
44
54
|
end
|
45
|
-
|
46
55
|
end
|
47
|
-
|
48
56
|
end
|
data/lib/ffmprb/file.rb
CHANGED
@@ -95,7 +95,8 @@ module Ffmprb
|
|
95
95
|
|
96
96
|
def initialize(path:, mode:)
|
97
97
|
@mode = mode.to_sym
|
98
|
-
fail Error, "Open for read, create for write, ??? for #{@mode}" unless
|
98
|
+
fail Error, "Open for read, create for write, ??? for #{@mode}" unless
|
99
|
+
%i[read write].include?(@mode)
|
99
100
|
@path = path
|
100
101
|
@path.close if @path && @path.respond_to?(:close) # NOTE we operate on closed files
|
101
102
|
path! # NOTE early (exception) raiser
|
@@ -147,11 +148,20 @@ module Ffmprb
|
|
147
148
|
end
|
148
149
|
end
|
149
150
|
|
150
|
-
def resolution
|
151
|
-
v_stream = probe['streams'].first
|
151
|
+
def resolution(force=false)
|
152
|
+
v_stream = probe(force)['streams'].first
|
152
153
|
"#{v_stream['width']}x#{v_stream['height']}"
|
153
154
|
end
|
154
155
|
|
156
|
+
def fps(force=false)
|
157
|
+
v_stream = probe(force)['streams'].first
|
158
|
+
Rational v_stream['r_frame_rate'] || v_stream['avg_frame_rate']
|
159
|
+
end
|
160
|
+
|
161
|
+
def creation_time(force=false)
|
162
|
+
Time.parse probe(force)['format']['tags']['creation_time']
|
163
|
+
end
|
164
|
+
|
155
165
|
|
156
166
|
# Manipulation
|
157
167
|
|
@@ -191,9 +201,7 @@ module Ffmprb
|
|
191
201
|
fail Error, "This doesn't look like a ffprobable file" unless probe['streams']
|
192
202
|
end
|
193
203
|
end
|
194
|
-
|
195
204
|
end
|
196
|
-
|
197
205
|
end
|
198
206
|
|
199
207
|
require_relative 'file/sample'
|
data/lib/ffmprb/filter.rb
CHANGED
@@ -49,6 +49,7 @@ module Ffmprb
|
|
49
49
|
inout 'asplit', inputs, outputs
|
50
50
|
end
|
51
51
|
|
52
|
+
# TODO? fix "Queue input is backward in time"
|
52
53
|
def areverse(input=nil, output=nil)
|
53
54
|
inout 'areverse', input, output
|
54
55
|
end
|
@@ -184,7 +185,7 @@ module Ffmprb
|
|
184
185
|
inout 'fps=fps=%{fps}', input, output, fps: fps
|
185
186
|
end
|
186
187
|
|
187
|
-
def
|
188
|
+
def framerate(fps, input=nil, output=nil)
|
188
189
|
inout 'framerate=fps=%{fps}', input, output, fps: fps
|
189
190
|
end
|
190
191
|
# TODO other effects like... minterpolate=fps=%{fps}:mi_mode=mci:mc_mode=aobmc:vsbmc=1
|
@@ -220,8 +221,11 @@ module Ffmprb
|
|
220
221
|
inout 'setsar=%{ratio}', input, output, ratio: ratio
|
221
222
|
end
|
222
223
|
|
223
|
-
def
|
224
|
-
inout
|
224
|
+
def setpts_framerate(ratio, fps, input=nil, output=nil)
|
225
|
+
inout [
|
226
|
+
inout('setpts=%{r_fps}*PTS', r_fps: 1.0/ratio),
|
227
|
+
*framerate(fps),
|
228
|
+
].join(', '), input, output
|
225
229
|
end
|
226
230
|
|
227
231
|
def scale(resolution, input=nil, output=nil)
|
@@ -300,10 +304,18 @@ module Ffmprb
|
|
300
304
|
end
|
301
305
|
|
302
306
|
|
303
|
-
def complex_args(*filters)
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
+
def complex_args(*filters, script_file: nil)
|
308
|
+
if filters.empty?
|
309
|
+
[]
|
310
|
+
else
|
311
|
+
filter_complex_opt = filters.join('; ')
|
312
|
+
if script_file
|
313
|
+
script_file.write filter_complex_opt
|
314
|
+
script_file.close
|
315
|
+
['-filter_complex_script', script_file.path]
|
316
|
+
else
|
317
|
+
['-filter_complex', filter_complex_opt]
|
318
|
+
end
|
307
319
|
end
|
308
320
|
end
|
309
321
|
|
@@ -19,13 +19,15 @@ module Ffmprb
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def filters_for(lbl, video:, audio:)
|
22
|
+
fail Error, "pacing requires fps" unless
|
23
|
+
video.fps
|
22
24
|
|
23
25
|
# Pacing
|
24
26
|
|
25
27
|
lbl_aux = "pc#{lbl}"
|
26
28
|
super(lbl_aux, video: video, audio: audio) +
|
27
29
|
[
|
28
|
-
*((video && channel?(:video))? Filter.
|
30
|
+
*((video && channel?(:video))? Filter.setpts_framerate(@ratio, video.fps, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
29
31
|
*((audio && channel?(:audio))? Filter.atempo(@ratio, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
30
32
|
]
|
31
33
|
end
|
data/lib/ffmprb/process/input.rb
CHANGED
@@ -40,8 +40,8 @@ module Ffmprb
|
|
40
40
|
attr_reader :process
|
41
41
|
|
42
42
|
def initialize(io, process, video:, audio:)
|
43
|
-
@io = self.class.resolve(io)
|
44
43
|
@process = process
|
44
|
+
@io = self.class.resolve(io)
|
45
45
|
@channels = {
|
46
46
|
video: video && @io.channel?(:video) && OpenStruct.new(video),
|
47
47
|
audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
|
@@ -76,18 +76,18 @@ module Ffmprb
|
|
76
76
|
Filter.copy "#{in_lbl}:v", "#{lbl}:v"
|
77
77
|
end
|
78
78
|
elsif video
|
79
|
-
fail Error, "No video stream to provide"
|
79
|
+
fail Error, "No video stream to provide ('#{io.path}')"
|
80
80
|
end),
|
81
81
|
*(if audio && channel?(:audio)
|
82
82
|
Filter.anull "#{in_lbl}:a", "#{lbl}:a"
|
83
83
|
elsif audio
|
84
|
-
fail Error, "No audio stream to provide"
|
84
|
+
fail Error, "No audio stream to provide ('#{io.path}')"
|
85
85
|
end)
|
86
86
|
]
|
87
87
|
end
|
88
88
|
|
89
89
|
def channel?(medium)
|
90
|
-
|
90
|
+
!!channel(medium)
|
91
91
|
end
|
92
92
|
|
93
93
|
def channel(medium)
|
@@ -89,19 +89,13 @@ module Ffmprb
|
|
89
89
|
# NOTE mapping input to this lbl
|
90
90
|
|
91
91
|
lbl = "o#{idx}rl#{i}"
|
92
|
-
lbl_aux = "t#{lbl}"
|
93
92
|
|
94
93
|
# NOTE Image-Padding to match the target resolution
|
95
94
|
# TODO full screen only at the moment (see exception above)
|
96
95
|
|
97
96
|
Ffmprb.logger.debug{"#{self} asking for filters of #{curr_reel.reel.io.inspect} video: #{channel(:video)}, audio: #{channel(:audio)}"}
|
98
|
-
|
99
|
-
|
100
|
-
*curr_reel.reel.filters_for(lbl_aux, video: channel(:video), audio: channel(:audio)),
|
101
|
-
*(channel?(:video)? Filter.interpolate_v(video_fps, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
102
|
-
*(channel?(:audio)? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
103
|
-
]
|
104
|
-
)
|
97
|
+
# NOTE may require changes if fps is different (and ffmpeg freezes)
|
98
|
+
@filters.concat curr_reel.reel.filters_for(lbl, video: channel(:video), audio: channel(:audio))
|
105
99
|
end
|
106
100
|
|
107
101
|
trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
|
@@ -398,9 +392,6 @@ module Ffmprb
|
|
398
392
|
(@overlays ||= []) <<
|
399
393
|
OpenStruct.new(reel: reel, at: at, duck: duck)
|
400
394
|
end
|
401
|
-
|
402
395
|
end
|
403
|
-
|
404
396
|
end
|
405
|
-
|
406
397
|
end
|