ffmprb 0.7.5 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,24 +1,45 @@
1
+ require 'ostruct'
2
+
1
3
  module Ffmprb
2
4
 
3
5
  class Process
4
6
 
5
7
  class Output
6
8
 
7
- def initialize(io, only:, resolution:, fps:)
9
+ class << self
10
+
11
+ def audio_cmd_options(audio=Process.output_audio_options)
12
+ audio ||= {}
13
+ [].tap do |options|
14
+ options.concat %W[-c:a #{audio[:encoder]}] if audio[:encoder]
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ # TODO unique labeling does not justify idx, will be refactored
21
+ def initialize(io, idx, video:, audio:)
8
22
  @io = resolve(io)
9
- @channels = [*only]
10
- @channels = nil if @channels.empty?
11
- @resolution = resolution
12
- @fps = 30
23
+ @idx = idx
24
+ @channels = {
25
+ video: video && @io.channel?(:video) && OpenStruct.new(video),
26
+ audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
27
+ }
28
+ if channel?(:video)
29
+ channel(:video).resolution.to_s.split('x').each do |dim|
30
+ fail Error, "Both dimensions of a resolution must be divisible by 2, sorry about that" unless dim.to_i % 2 == 0
31
+ end
32
+ end
13
33
  end
14
34
 
15
35
  # XXX This method is exceptionally long at the moment. This is not too grand.
16
36
  # However, structuring the code should be undertaken with care, as not to harm the composition clarity.
17
- def options_for(process) # NOTE process is not thread-safe (nothing actually is), so must not share it with another thread
37
+ def filters
18
38
  fail Error, "Nothing to roll..." unless @reels
19
39
  fail Error, "Supporting just full_screen for now, sorry." unless @reels.all?(&:full_screen?)
40
+ return @filters if @filters
20
41
 
21
- filters = []
42
+ @filters = []
22
43
 
23
44
  # Concatting
24
45
  segments = []
@@ -31,24 +52,18 @@ module Ffmprb
31
52
 
32
53
  # NOTE mapping input to this lbl
33
54
 
34
- lbl = "rl#{i}"
35
- lbl_aux = "sp#{i}"
55
+ lbl = "o#{@idx}rl#{i}"
36
56
 
37
- # NOTE Image-Scaling & Image-Padding to match the target resolution
38
- # XXX full screen only (see exception above)
57
+ # NOTE Image-Padding to match the target resolution
58
+ # TODO full screen only at the moment (see exception above)
39
59
 
40
- filters.concat( # XXX an opportunity for optimisation through passing the actual channel options
41
- curr_reel.reel.filters_for lbl_aux, process: process, output: self, video: channel?(:video), audio: channel?(:audio)
60
+ @filters.concat(
61
+ curr_reel.reel.filters_for lbl, video: channel(:video), audio: channel(:audio)
42
62
  )
43
- filters.concat(
44
- Filter.scale_pad_fps target_width, target_height, target_fps, "#{lbl_aux}:v", "#{lbl}:v"
45
- ) if channel?(:video)
46
- filters.concat(
47
- Filter.anull "#{lbl_aux}:a", "#{lbl}:a"
48
- ) if channel?(:audio)
49
63
  end
50
64
 
51
65
  trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
66
+ transition_length = curr_reel.transition ? curr_reel.transition.length : 0
52
67
 
53
68
  if trim_prev_at
54
69
 
@@ -58,20 +73,21 @@ module Ffmprb
58
73
 
59
74
  lbl_pad = "bl#{prev_lbl}#{i}"
60
75
  # NOTE generously padding the previous segment to support for all the cases
61
- filters.concat(
62
- Filter.blank_source trim_prev_at + curr_reel.transition_length, target_resolution, target_fps, "#{lbl_pad}:v"
76
+ @filters.concat(
77
+ Filter.blank_source trim_prev_at + transition_length,
78
+ channel(:video).resolution, channel(:video).fps, "#{lbl_pad}:v"
63
79
  ) if channel?(:video)
64
- filters.concat(
65
- Filter.silent_source trim_prev_at + curr_reel.transition_length, "#{lbl_pad}:a"
80
+ @filters.concat(
81
+ Filter.silent_source trim_prev_at + transition_length, "#{lbl_pad}:a"
66
82
  ) if channel?(:audio)
67
83
 
68
84
  if prev_lbl
69
85
  lbl_aux = lbl_pad
70
86
  lbl_pad = "pd#{prev_lbl}#{i}"
71
- filters.concat(
87
+ @filters.concat(
72
88
  Filter.concat_v ["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v"
73
89
  ) if channel?(:video)
74
- filters.concat(
90
+ @filters.concat(
75
91
  Filter.concat_a ["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a"
76
92
  ) if channel?(:audio)
77
93
  end
@@ -81,10 +97,10 @@ module Ffmprb
81
97
  # NOTE Split the previous segment for transition
82
98
 
83
99
  if trim_prev_at > 0
84
- filters.concat(
100
+ @filters.concat(
85
101
  Filter.split "#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]
86
102
  ) if channel?(:video)
87
- filters.concat(
103
+ @filters.concat(
88
104
  Filter.asplit "#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]
89
105
  ) if channel?(:audio)
90
106
  lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
@@ -99,10 +115,10 @@ module Ffmprb
99
115
 
100
116
  new_prev_lbl = "tm#{prev_lbl}#{i}a"
101
117
 
102
- filters.concat(
118
+ @filters.concat(
103
119
  Filter.trim 0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v"
104
120
  ) if channel?(:video)
105
- filters.concat(
121
+ @filters.concat(
106
122
  Filter.atrim 0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a"
107
123
  ) if channel?(:audio)
108
124
 
@@ -114,27 +130,34 @@ module Ffmprb
114
130
 
115
131
  # NOTE snip the end of the previous segment and combine with this reel
116
132
 
117
- lbl_end1 = "tm#{i}b"
118
- lbl_reel = "tn#{i}"
133
+ lbl_end1 = "o#{@idx}tm#{i}b"
134
+ lbl_reel = "o#{@idx}tn#{i}"
135
+
119
136
  if !lbl # no reel
120
- lbl_aux = "bk#{i}"
121
- filters.concat(
122
- Filter.blank_source curr_reel.transition_length, target_resolution, channel(:video).fps, "#{lbl_aux}:v"
137
+ lbl_aux = "o#{@idx}bk#{i}"
138
+ @filters.concat(
139
+ Filter.blank_source transition_length, channel(:video).resolution, channel(:video).fps, "#{lbl_aux}:v"
123
140
  ) if channel?(:video)
124
- filters.concat(
125
- Filter.silent_source curr_reel.transition_length, "#{lbl_aux}:a"
141
+ @filters.concat(
142
+ Filter.silent_source transition_length, "#{lbl_aux}:a"
126
143
  ) if channel?(:audio)
127
144
  end # NOTE else hope lbl is long enough for the transition
128
- filters.concat(
129
- Filter.trim trim_prev_at, trim_prev_at + curr_reel.transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
145
+
146
+ @filters.concat(
147
+ Filter.trim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
130
148
  ) if channel?(:video)
131
- filters.concat(
132
- Filter.atrim trim_prev_at, trim_prev_at + curr_reel.transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a"
149
+ @filters.concat(
150
+ Filter.atrim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a"
133
151
  ) if channel?(:audio)
134
- filters.concat(
135
- Filter.transition_av curr_reel.transition, target_resolution, target_fps, [lbl_end1, lbl || lbl_aux], lbl_reel,
136
- video: channel?(:video), audio: channel?(:audio)
137
- )
152
+
153
+ # TODO the only supported transition, see #*lay
154
+ @filters.concat(
155
+ Filter.blend_v transition_length, channel(:video).resolution, channel(:video).fps, ["#{lbl_end1}:v", "#{lbl || lbl_aux}:v"], "#{lbl_reel}:v"
156
+ ) if channel?(:video)
157
+ @filters.concat(
158
+ Filter.blend_a transition_length, ["#{lbl_end1}:a", "#{lbl || lbl_aux}:a"], "#{lbl_reel}:a"
159
+ ) if channel?(:audio)
160
+
138
161
  lbl = lbl_reel
139
162
  end
140
163
 
@@ -145,12 +168,12 @@ module Ffmprb
145
168
 
146
169
  segments.compact!
147
170
 
148
- lbl_out = 'oo'
171
+ lbl_out = "o#{@idx}o"
149
172
 
150
- filters.concat(
173
+ @filters.concat(
151
174
  Filter.concat_v segments.map{|s| "#{s}:v"}, "#{lbl_out}:v"
152
175
  ) if channel?(:video)
153
- filters.concat(
176
+ @filters.concat(
154
177
  Filter.concat_a segments.map{|s| "#{s}:a"}, "#{lbl_out}:a"
155
178
  ) if channel?(:audio)
156
179
 
@@ -159,22 +182,22 @@ module Ffmprb
159
182
  # NOTE in-process overlays first
160
183
 
161
184
  @overlays.to_a.each_with_index do |over_reel, i|
162
- next if over_reel.duck # XXX this is currently a single case of multi-process... process
185
+ next if over_reel.duck # NOTE this is currently a single case of multi-process... process
163
186
 
164
187
  fail Error, "Video overlays are not implemented just yet, sorry..." if over_reel.reel.channel?(:video)
165
188
 
166
189
  # Audio overlaying
167
190
 
168
- lbl_nxt = "oo#{i}"
191
+ lbl_nxt = "o#{@idx}o#{i}"
169
192
 
170
- lbl_over = "ol#{i}"
171
- filters.concat( # NOTE audio only, see above
172
- over_reel.reel.filters_for lbl_over, process: process, output: self
193
+ lbl_over = "o#{@idx}l#{i}"
194
+ @filters.concat( # NOTE audio only, see above
195
+ over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
173
196
  )
174
- filters.concat(
197
+ @filters.concat(
175
198
  Filter.copy "#{lbl_out}:v", "#{lbl_nxt}:v"
176
199
  ) if channel?(:video)
177
- filters.concat(
200
+ @filters.concat(
178
201
  Filter.amix_to_first_same_volume ["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a"
179
202
  ) if channel?(:audio)
180
203
 
@@ -183,67 +206,77 @@ module Ffmprb
183
206
 
184
207
  # NOTE multi-process overlays last
185
208
 
186
- channel_lbl_ios = {} # XXX this is a spaghetti machine
187
- channel_lbl_ios["#{lbl_out}:v"] = @io if channel?(:video)
188
- channel_lbl_ios["#{lbl_out}:a"] = @io if channel?(:audio)
209
+ @channel_lbl_ios = {} # XXX this is a spaghetti machine
210
+ @channel_lbl_ios["#{lbl_out}:v"] = @io if channel?(:video)
211
+ @channel_lbl_ios["#{lbl_out}:a"] = @io if channel?(:audio)
189
212
 
190
- # XXX supporting just "full" overlays for now, see exception in #add_reel
213
+ # TODO supporting just "full" overlays for now, see exception in #add_reel
191
214
  @overlays.to_a.each_with_index do |over_reel, i|
192
215
 
193
- # XXX this is currently a single case of multi-process... process
216
+ # NOTE this is currently a single case of multi-process... process
194
217
  if over_reel.duck
195
218
  fail Error, "Don't know how to duck video... yet" if over_reel.duck != :audio
196
219
 
197
220
  # So ducking just audio here, ye?
221
+ # XXX check if we're on audio channel
198
222
 
199
- main_a_o = channel_lbl_ios["#{lbl_out}:a"]
200
- fail Error, "Main output does not contain audio to duck" unless main_a_o
223
+ main_av_o = @channel_lbl_ios["#{lbl_out}:a"]
224
+ fail Error, "Main output does not contain audio to duck" unless main_av_o
201
225
  # XXX#181845 must really seperate channels for streaming (e.g. mp4 wouldn't stream through the fifo)
202
- main_a_inter_o = File.temp_fifo(main_a_o.extname)
203
- channel_lbl_ios.each do |channel_lbl, io|
204
- channel_lbl_ios[channel_lbl] = main_a_inter_o if io == main_a_o # XXX ~~~spaghetti
226
+ # NOTE what really must be done here (optimisation & compatibility):
227
+ # - output v&a through non-compressed pipes
228
+ # - v-output will be input to the new v+a merging+encoding process
229
+ # - a-output will go through the ducking process below and its output will be input to the m+e process above
230
+ # - v-output will have to use another thread-buffered pipe
231
+ main_av_inter_o = File.temp_fifo(main_av_o.extname)
232
+ @channel_lbl_ios.each do |channel_lbl, io|
233
+ @channel_lbl_ios[channel_lbl] = main_av_inter_o if io == main_av_o # XXX ~~~spaghetti
205
234
  end
206
- Ffmprb.logger.debug "Re-routed the main audio output (#{main_a_inter_o.path}->...->#{main_a_o.path}) through the process of audio ducking"
235
+ Ffmprb.logger.debug "Re-routed the main audio output (#{main_av_inter_o.path}->...->#{main_av_o.path}) through the process of audio ducking"
207
236
 
208
- overlay_i, overlay_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname :audio)
209
- lbl_over = "ol#{i}"
210
- filters.concat(
211
- over_reel.reel.filters_for lbl_over, process: process, output: self, video: false, audio: true
237
+ over_a_i, over_a_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname :audio)
238
+ lbl_over = "o#{@idx}l#{i}"
239
+ @filters.concat(
240
+ over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
212
241
  )
213
- channel_lbl_ios["#{lbl_over}:a"] = overlay_i
214
- Ffmprb.logger.debug "Routed and buffering an auxiliary output fifos (#{overlay_i.path}>#{overlay_o.path}) for overlay"
242
+ @channel_lbl_ios["#{lbl_over}:a"] = over_a_i
243
+ Ffmprb.logger.debug "Routed and buffering an auxiliary output fifos (#{over_a_i.path}>#{over_a_o.path}) for overlay"
215
244
 
216
- inter_i, inter_o = File.threaded_buffered_fifo(main_a_inter_o.extname)
245
+ inter_i, inter_o = File.threaded_buffered_fifo(main_av_inter_o.extname)
217
246
  Ffmprb.logger.debug "Allocated fifos to buffer media (#{inter_i.path}>#{inter_o.path}) while finding silence"
218
247
 
219
248
  Util::Thread.new "audio ducking" do
220
- silence = Ffmprb.find_silence(main_a_inter_o, inter_i)
249
+ silence = Ffmprb.find_silence(main_av_inter_o, inter_i)
221
250
 
222
251
  Ffmprb.logger.debug "Audio ducking with silence: [#{silence.map{|s| "#{s.start_at}-#{s.end_at}"}.join ', '}]"
223
252
 
224
- Process.duck_audio inter_o, overlay_o, silence, main_a_o,
225
- video: (channel?(:video)? {resolution: target_resolution, fps: target_fps}: false)
253
+ Process.duck_audio inter_o, over_a_o, silence, main_av_o, video: channel(:video), audio: channel(:audio)
226
254
  end
227
255
  end
228
256
 
229
257
  end
230
258
 
231
- Filter.complex_options(filters).tap do |options|
259
+ @filters
260
+ end
232
261
 
233
- io_channel_lbls = {} # XXX ~~~spaghetti
234
- channel_lbl_ios.each do |channel_lbl, io|
235
- (io_channel_lbls[io] ||= []) << channel_lbl
236
- end
237
- io_channel_lbls.each do |io, channel_lbls|
238
- channel_lbls.each do |channel_lbl|
239
- options << '-map' << "[#{channel_lbl}]"
240
- # XXX temporary patchwork
241
- options << '-c:a' << 'libmp3lame' if channel_lbls.size > 1 && channel_lbl =~ /:a$/
242
- end
243
- options << io.path
244
- end
262
+ def options
263
+ fail Error, "Must generate filters first." unless @channel_lbl_ios
245
264
 
265
+ options = []
266
+
267
+ io_channel_lbls = {} # XXX ~~~spaghetti
268
+ @channel_lbl_ios.each do |channel_lbl, io|
269
+ (io_channel_lbls[io] ||= []) << channel_lbl
270
+ end
271
+ io_channel_lbls.each do |io, channel_lbls|
272
+ channel_lbls.each do |channel_lbl|
273
+ options << '-map' << "[#{channel_lbl}]"
274
+ end
275
+ options.concat self.class.audio_cmd_options(channel :audio) if channel? :audio
276
+ options << io.path
246
277
  end
278
+
279
+ options
247
280
  end
248
281
 
249
282
  def roll(
@@ -263,29 +296,24 @@ module Ffmprb
263
296
  def overlay(
264
297
  reel,
265
298
  at: 0,
266
- transition: nil,
267
299
  duck: nil
268
300
  )
269
301
  fail Error, "Nothing to overlay..." unless reel
270
302
  fail Error, "Nothing to lay over yet..." if @reels.to_a.empty?
271
303
  fail Error, "Ducking overlays should come last... for now" if !duck && @overlays.to_a.last && @overlays.to_a.last.duck
272
304
 
273
- (@overlays ||= []) <<
274
- OpenStruct.new(reel: reel, at: at, duck: duck)
305
+ add_snip reel, at, duck
275
306
  end
276
307
 
277
- def channel?(medium)
278
- @channels.include?(medium) && @io.channel?(medium) && reels_channel?(medium)
308
+ def channel(medium)
309
+ @channels[medium]
279
310
  end
280
311
 
281
- def channel?(medium, force=false)
282
- return !!@channels && @channels.include?(medium) && @io.channel?(medium) if force
283
-
284
- (!@channels || @channels.include?(medium)) && @io.channel?(medium) &&
285
- reels_channel?(medium)
312
+ def channel?(medium)
313
+ !!channel(medium)
286
314
  end
287
315
 
288
- # XXX TMP protected
316
+ protected
289
317
 
290
318
  def resolve(io)
291
319
  return io unless io.is_a? String
@@ -300,7 +328,7 @@ module Ffmprb
300
328
  end
301
329
  end
302
330
 
303
- # XXX TMP private
331
+ private
304
332
 
305
333
  def reels_channel?(medium)
306
334
  @reels.to_a.all?{|r| !r.reel || r.reel.channel?(medium)}
@@ -310,29 +338,22 @@ module Ffmprb
310
338
  fail Error, "No time to roll..." if after && after.to_f <= 0
311
339
  fail Error, "Partial (not coming last in process) overlays are currently unsupported, sorry." unless @overlays.to_a.empty?
312
340
 
313
- # NOTE limited functionality (see exception in Filter.transition_av): transition = {effect => duration}
314
- transition_length = transition.to_h.max_by{|k,v| v}.to_a.last.to_f
341
+ # NOTE limited functionality: transition = {effect => duration}
342
+ # TODO temporary obviously, see rendering
343
+ trans =
344
+ if transition
345
+ fail "Unsupported (yet) transition, sorry." unless
346
+ transition.size == 1 && transition[:blend]
347
+ OpenStruct.new length: transition[:blend].to_f
348
+ end
315
349
 
316
350
  (@reels ||= []) <<
317
- OpenStruct.new(reel: reel, after: after, transition: transition, transition_length: transition_length, full_screen?: full_screen)
351
+ OpenStruct.new(reel: reel, after: after, transition: trans, full_screen?: full_screen)
318
352
  end
319
353
 
320
- def target_width
321
- @target_width ||= @resolution.to_s.split('x')[0].to_i.tap do |width|
322
- raise Error, "Width (#{width}) must be divisible by 2, sorry" unless width % 2 == 0
323
- end
324
- end
325
- def target_height
326
- @target_height ||= @resolution.to_s.split('x')[1].to_i.tap do |height|
327
- raise Error, "Height (#{height}) must be divisible by 2, sorry" unless height % 2 == 0
328
- end
329
- end
330
- def target_resolution
331
- "#{target_width}x#{target_height}"
332
- end
333
-
334
- def target_fps
335
- @fps
354
+ def add_snip(reel, at, duck)
355
+ (@overlays ||= []) <<
356
+ OpenStruct.new(reel: reel, at: at, duck: duck)
336
357
  end
337
358
 
338
359
  end
@@ -9,6 +9,10 @@ module Ffmprb
9
9
  attr_accessor :duck_audio_transition_length,
10
10
  :duck_audio_transition_in_start, :duck_audio_transition_out_start
11
11
 
12
+ attr_accessor :output_video_resolution
13
+ attr_accessor :output_video_fps
14
+ attr_accessor :output_audio_encoder
15
+
12
16
  attr_accessor :timeout
13
17
 
14
18
  def intermediate_channel_extname(*media)
@@ -23,20 +27,35 @@ module Ffmprb
23
27
  end
24
28
  end
25
29
 
30
+ def output_video_options
31
+ {
32
+ resolution: output_video_resolution,
33
+ fps: output_video_fps
34
+ }
35
+ end
36
+ def output_audio_options
37
+ {
38
+ encoder: output_audio_encoder
39
+ }
40
+ end
41
+
42
+ # NOTE Temporarily, av_main_i/o and not a_main_i/o
26
43
  def duck_audio(av_main_i, a_overlay_i, silence, av_main_o,
27
- volume_lo: duck_audio_volume_lo, volume_hi: duck_audio_volume_hi,
44
+ volume_lo: duck_audio_volume_lo,
45
+ volume_hi: duck_audio_volume_hi,
28
46
  silent_min: duck_audio_silent_min,
29
- video: {resolution: Ffmprb::CGA, fps: 30} # XXX temporary
47
+ video:, # NOTE Temporarily, video should not be here
48
+ audio:
30
49
  )
31
- Ffmprb.process(av_main_i, a_overlay_i, silence, av_main_o) do |main_input, overlay_input, duck_data, main_output|
50
+ Ffmprb.process do
32
51
 
33
- in_main = input(main_input, **(video ? {} : {only: :audio}))
34
- in_over = input(overlay_input, only: :audio)
35
- output(main_output, **(video ? {resolution: video[:resolution], fps: video[:fps]} : {})) do
52
+ in_main = input(av_main_i)
53
+ in_over = input(a_overlay_i)
54
+ output(av_main_o, video: video, audio: audio) do
36
55
  roll in_main
37
56
 
38
57
  ducked_overlay_volume = {0.0 => volume_lo}
39
- duck_data.each do |silent|
58
+ silence.each do |silent|
40
59
  next if silent.end_at && silent.start_at && (silent.end_at - silent.start_at) < silent_min
41
60
 
42
61
  transition_in_start = silent.start_at + Process.duck_audio_transition_in_start
@@ -63,57 +82,79 @@ module Ffmprb
63
82
 
64
83
  attr_reader :timeout
65
84
 
66
- def initialize(*args, **opts, &blk)
67
- @inputs = []
68
- @timeout = opts[:timeout] || self.class.timeout
85
+ def initialize(*args, **opts)
86
+ @inputs, @outputs = [], []
87
+ @timeout = opts.delete(:timeout) || self.class.timeout
88
+
89
+ @ignore_broken_pipe = opts.delete(:ignore_broken_pipe) # XXX SPEC ME
90
+ fail Error, "Unknown options: #{opts}" unless opts.empty?
69
91
  end
70
92
 
71
- def input(io, only: nil)
72
- Input.new(io, only: only).tap do |inp|
93
+ def input(io)
94
+ Input.new(io, self).tap do |inp|
73
95
  @inputs << inp
74
96
  end
75
97
  end
76
98
 
77
- def output(io, only: nil, resolution: Ffmprb::CGA, fps: 30, &blk)
78
- fail Error, "Just one output for now, sorry." if @output
99
+ def temp_input(extname) # XXX SPEC ME
100
+ input(nil).tap do |inp|
101
+ inp.temporise! extname
102
+ end
103
+ end
104
+
105
+ def input_label(input)
106
+ @inputs.index input
107
+ end
79
108
 
80
- @output = Output.new(io, only: only, resolution: resolution, fps: fps).tap do |out|
81
- out.instance_exec &blk
109
+ def output(io, video: true, audio: true, &blk)
110
+ Output.new(io, @outputs.size,
111
+ video: channel_params(video, self.class.output_video_options),
112
+ audio: channel_params(audio, self.class.output_audio_options)
113
+ ).tap do |out|
114
+ @outputs << out
115
+ out.instance_exec &blk if blk
82
116
  end
83
117
  end
84
118
 
85
119
  # NOTE the one and the only entry-point processing function which spawns threads etc
86
- def run(limit: nil) # (async: false)
120
+ def run(limit: nil) # TODO (async: false)
87
121
  # NOTE this is both for the future async: option and according to
88
122
  # the threading policy (a parent death will be noticed and handled by children)
89
123
  thr = Util::Thread.new do
90
124
  # NOTE yes, an exception can occur anytime, and we'll just die, it's ok, see above
91
- Util.ffmpeg(*command, limit: limit, timeout: timeout).tap do |res| # XXX just to return something
125
+ # XXX just to return something -- no apparent practical use
126
+ cmd = command
127
+ Util.ffmpeg(*cmd, limit: limit, timeout: timeout, ignore_broken_pipe: @ignore_broken_pipe).tap do |res|
92
128
  Util::Thread.join_children! limit, timeout: timeout
93
129
  end
94
130
  end
95
131
  thr.value if thr.join limit # NOTE should not block for more than limit
96
132
  end
97
133
 
98
- def [](obj)
99
- case obj
100
- when Input
101
- @inputs.find_index(obj)
102
- end
103
- end
104
-
105
134
  private
106
135
 
107
136
  def command
108
- input_options + output_options
137
+ input_options + filter_options + output_options
109
138
  end
110
139
 
111
140
  def input_options
112
141
  @inputs.map(&:options).flatten(1)
113
142
  end
114
143
 
144
+ def filter_options
145
+ Filter.complex_options @outputs.map(&:filters).reduce(:+)
146
+ end
147
+
115
148
  def output_options
116
- @output.options_for self
149
+ @outputs.map(&:options).flatten(1)
150
+ end
151
+
152
+ def channel_params(value, default)
153
+ if value
154
+ default.merge(value == true ? {} : value.to_h)
155
+ elsif value != false
156
+ {}
157
+ end
117
158
  end
118
159
 
119
160
  end
@@ -1,4 +1,4 @@
1
- # XXX unused and commented out
1
+ # TODO unused and commented out
2
2
  # module Ffmprb
3
3
  #
4
4
  # module Util
@@ -5,7 +5,7 @@ module Ffmprb
5
5
  # NOTE doesn't have specs (and not too proud about it)
6
6
  class Thread < ::Thread
7
7
 
8
- class Error < StandardError; end
8
+ class Error < Ffmprb::Error; end
9
9
  class ParentError < Error; end
10
10
 
11
11
  class << self
@@ -70,7 +70,7 @@ module Ffmprb
70
70
  sync_q.deq
71
71
  end
72
72
 
73
- # XXX protected: none of these methods should be called by a user code, the only public methods are above
73
+ # TODO protected: none of these methods should be called by a user code, the only public methods are above
74
74
 
75
75
  def live!
76
76
  fail ParentError if @parent.status.nil?