ffmprb 0.12.1 → 0.12.3
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 +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
|