ffmprb 0.7.5 → 0.9.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.
@@ -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?