ffmprb 0.12.1 → 0.12.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- time_s = Time.now.strftime('%y-%m-%d-%H-%M')
10
- title = "Topublish uploaded at #{time_s}"
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
- # Ffmprb.debug = true # XXX
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
- fin_video_opt = {resolution: Ffmprb::HD_1080p, fps: 60, encoder: 'libx264 -crf 31'} # XXX -preset veryslow
34
- YOU_VIDEO_OPT = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264'}
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
- GOP_MP4_RE = /\b(GX(\d\d)(\d\d\d\d)\.MP4)\b/i
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 out_path(name)
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
- shots << $3
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
- shots << $3
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
- abort "ERROR no inputs given" if
127
- av_src_cuts.empty?
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
- (av_pipe = Ffmprb::File.temp_fifo('.flv')),
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 av_pipe, video: int_video_opt do
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 |av_pipe|
256
+ Ffmprb::File.temp_fifo('.flv') do |av_stream|
150
257
  thr = Thread.new do
151
- Ffmprb.process do
152
- inp_cut_opts =
153
- pipe_cut_threads.map do |av_pipe, cut_opts, _|
154
- (cut_opts.empty?? [{}] : cut_opts).map do |cut_opt|
155
- [input(av_pipe), cut_opt]
156
- end
157
- end.reduce :+
158
- output av_pipe, video: YOU_VIDEO_OPT do
159
- inp_cut_opts.each do |inp, cut_opt|
160
- roll inp.cut cut_opt
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
- # output out_path("#{out_name}x-8"), video: fin_video_opt do
173
- # inp_cut_opts.each do |inp, cut_opt|
174
- # roll inp.cut(cut_opt).reverse.pace 8
175
- # end
176
- # end
177
- # output out_path("#{out_name}x8-"), video: fin_video_opt do
178
- # inp_cut_opts.each do |inp, cut_opt|
179
- # roll inp.cut(cut_opt).pace(8).reverse
180
- # end
181
- # end
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(av_pipe)
324
+ in1 = input(av_stream)
187
325
  output you_out_path, video: YOU_VIDEO_OPT do
188
- roll in1.pace(16).reverse
189
- roll in1.pp
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.send_timeout_sec =
26
- youtube.client_options.open_timeout_sec =
27
- youtube.client_options.read_timeout_sec = YOUTUBE_TIMEOUT_SEC
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
@@ -1,7 +1,7 @@
1
1
  module Ffmprb
2
2
 
3
3
  File.image_extname_regex = /^\.(jpe?g|a?png|y4m)$/i
4
- File.sound_extname_regex = /^\.(mp3|wav)$/i
4
+ File.sound_extname_regex = /^\.(mp3|wav|m4a)$/i
5
5
  File.movie_extname_regex = /^\.(mp4|flv|mov)$/i
6
6
 
7
7
  Filter.silence_noise_max_db = -40
@@ -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 path: #{video ? video.path : 'NONE'}, audio path: #{audio ? audio.path : 'NONE'}"}
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 !video || video.channel?(:video) && !video.channel?(:audio)
17
- fail Error, "Incorrect audio extname (must be sound)" unless !audio || audio.channel?(:audio) && !audio.channel?(:video)
18
- fail Error, "Can sample either video OR audio UNLESS a block is given" unless block_given? || !!audio != !!video
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
- cmd.concat %W[-vn -ss #{at} -t 1 #{audio.path}] if audio
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 %i[read write].include?(@mode)
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 interpolate_v(fps, input=nil, output=nil)
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 setpts(ratio, input=nil, output=nil)
224
- inout 'setpts=%{r_fps}*PTS', input, output, r_fps: 1.0/ratio
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
- [].tap do |args|
305
- args << '-filter_complex' << filters.join('; ') unless
306
- filters.empty?
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
 
@@ -25,6 +25,11 @@ module Ffmprb
25
25
  # Doing nothing
26
26
  unfiltered.filters_for lbl, video: video, audio: audio
27
27
  end
28
+
29
+
30
+ def channel(medium)
31
+ io.channel medium
32
+ end
28
33
  end
29
34
  end
30
35
  end
@@ -56,11 +56,7 @@ module Ffmprb
56
56
  ]
57
57
  end
58
58
  end
59
-
60
59
  end
61
-
62
60
  end
63
-
64
61
  end
65
-
66
62
  end
@@ -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.setpts(@ratio, "#{lbl_aux}:v", "#{lbl}:v"): nil),
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
@@ -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
- io.channel? medium
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
- @filters.concat(
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