ffmprb 0.7.0 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,7 @@ module Ffmprb
4
4
 
5
5
  class Output
6
6
 
7
- def initialize(io, only: nil, resolution: Ffmprb::QVGA, fps: 30)
7
+ def initialize(io, only:, resolution:, fps:)
8
8
  @io = resolve(io)
9
9
  @channels = [*only]
10
10
  @channels = nil if @channels.empty?
@@ -14,10 +14,9 @@ module Ffmprb
14
14
 
15
15
  # XXX This method is exceptionally long at the moment. This is not too grand.
16
16
  # However, structuring the code should be undertaken with care, as not to harm the composition clarity.
17
- def options(process)
18
- # XXX TODO manage stream labels through process
19
- raise Error, "Nothing to roll..." if @reels.select(&:reel).empty?
20
- raise Error, "Supporting just full_screen for now, sorry." unless @reels.all?(&:full_screen?)
17
+ def options_for(process) # NOTE process is not thread-safe (nothing actually is), so must not share it with another thread
18
+ fail Error, "Nothing to roll..." unless @reels
19
+ fail Error, "Supporting just full_screen for now, sorry." unless @reels.all?(&:full_screen?)
21
20
 
22
21
  filters = []
23
22
 
@@ -38,15 +37,15 @@ module Ffmprb
38
37
  # NOTE Image-Scaling & Image-Padding to match the target resolution
39
38
  # XXX full screen only (see exception above)
40
39
 
41
- filters +=
42
- curr_reel.reel.filters_for(lbl_aux, process: process,
43
- video: channel?(:video), audio: channel?(:audio))
44
- filters +=
45
- Filter.scale_pad_fps(target_width, target_height, target_fps, "#{lbl_aux}:v", "#{lbl}:v") if
46
- channel?(:video)
47
- filters +=
48
- Filter.anull("#{lbl_aux}:a", "#{lbl}:a") if
49
- channel?(:audio)
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)
42
+ )
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)
50
49
  end
51
50
 
52
51
  trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
@@ -59,22 +58,22 @@ module Ffmprb
59
58
 
60
59
  lbl_pad = "bl#{prev_lbl}#{i}"
61
60
  # NOTE generously padding the previous segment to support for all the cases
62
- filters +=
63
- Filter.black_source(trim_prev_at + curr_reel.transition_length, target_resolution, target_fps, "#{lbl_pad}:v") if
64
- channel?(:video)
65
- filters +=
66
- Filter.silent_source(trim_prev_at + curr_reel.transition_length, "#{lbl_pad}:a") if
67
- channel?(:audio)
61
+ filters.concat(
62
+ Filter.blank_source trim_prev_at + curr_reel.transition_length, target_resolution, target_fps, "#{lbl_pad}:v"
63
+ ) if channel?(:video)
64
+ filters.concat(
65
+ Filter.silent_source trim_prev_at + curr_reel.transition_length, "#{lbl_pad}:a"
66
+ ) if channel?(:audio)
68
67
 
69
68
  if prev_lbl
70
69
  lbl_aux = lbl_pad
71
70
  lbl_pad = "pd#{prev_lbl}#{i}"
72
- filters +=
73
- Filter.concat_v(["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v") if
74
- channel?(:video)
75
- filters +=
76
- Filter.concat_a(["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a") if
77
- channel?(:audio)
71
+ filters.concat(
72
+ Filter.concat_v ["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v"
73
+ ) if channel?(:video)
74
+ filters.concat(
75
+ Filter.concat_a ["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a"
76
+ ) if channel?(:audio)
78
77
  end
79
78
 
80
79
  if curr_reel.transition
@@ -82,12 +81,12 @@ module Ffmprb
82
81
  # NOTE Split the previous segment for transition
83
82
 
84
83
  if trim_prev_at > 0
85
- filters +=
86
- Filter.split("#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]) if
87
- channel?(:video)
88
- filters +=
89
- Filter.asplit("#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]) if
90
- channel?(:audio)
84
+ filters.concat(
85
+ Filter.split "#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]
86
+ ) if channel?(:video)
87
+ filters.concat(
88
+ Filter.asplit "#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]
89
+ ) if channel?(:audio)
91
90
  lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
92
91
  else
93
92
  lbl_pad, lbl_pad_ = nil, lbl_pad
@@ -100,12 +99,12 @@ module Ffmprb
100
99
 
101
100
  new_prev_lbl = "tm#{prev_lbl}#{i}a"
102
101
 
103
- filters +=
104
- Filter.trim(0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v") if
105
- channel?(:video)
106
- filters +=
107
- Filter.atrim(0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a") if
108
- channel?(:audio)
102
+ filters.concat(
103
+ Filter.trim 0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v"
104
+ ) if channel?(:video)
105
+ filters.concat(
106
+ Filter.atrim 0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a"
107
+ ) if channel?(:audio)
109
108
 
110
109
  segments << new_prev_lbl
111
110
  Ffmprb.logger.debug "Concatting segments: #{new_prev_lbl} pushed"
@@ -119,22 +118,23 @@ module Ffmprb
119
118
  lbl_reel = "tn#{i}"
120
119
  if !lbl # no reel
121
120
  lbl_aux = "bk#{i}"
122
- filters +=
123
- Filter.black_source(curr_reel.transition_length, target_resolution, target_fps, "#{lbl_aux}:v") if
124
- channel?(:video)
125
- filters +=
126
- Filter.silent_source(curr_reel.transition_length, "#{lbl_aux}:a") if
127
- channel?(:audio)
121
+ filters.concat(
122
+ Filter.blank_source curr_reel.transition_length, target_resolution, channel(:video).fps, "#{lbl_aux}:v"
123
+ ) if channel?(:video)
124
+ filters.concat(
125
+ Filter.silent_source curr_reel.transition_length, "#{lbl_aux}:a"
126
+ ) if channel?(:audio)
128
127
  end # NOTE else hope lbl is long enough for the transition
129
- filters +=
130
- Filter.trim(trim_prev_at, trim_prev_at + curr_reel.transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v") if
131
- channel?(:video)
132
- filters +=
133
- Filter.atrim(trim_prev_at, trim_prev_at + curr_reel.transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a") if
134
- channel?(:audio)
135
- filters +=
136
- Filter.transition_av(curr_reel.transition, target_resolution, target_fps, [lbl_end1, lbl || lbl_aux], lbl_reel,
137
- video: channel?(:video), audio: channel?(:audio))
128
+ filters.concat(
129
+ Filter.trim trim_prev_at, trim_prev_at + curr_reel.transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
130
+ ) 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"
133
+ ) 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
+ )
138
138
  lbl = lbl_reel
139
139
  end
140
140
 
@@ -147,37 +147,38 @@ module Ffmprb
147
147
 
148
148
  lbl_out = 'oo'
149
149
 
150
- filters +=
151
- Filter.concat_v(segments.map{|s| "#{s}:v"}, "#{lbl_out}:v") if channel?(:video)
152
- filters +=
153
- Filter.concat_a(segments.map{|s| "#{s}:a"}, "#{lbl_out}:a") if channel?(:audio)
150
+ filters.concat(
151
+ Filter.concat_v segments.map{|s| "#{s}:v"}, "#{lbl_out}:v"
152
+ ) if channel?(:video)
153
+ filters.concat(
154
+ Filter.concat_a segments.map{|s| "#{s}:a"}, "#{lbl_out}:a"
155
+ ) if channel?(:audio)
154
156
 
155
157
  # Overlays
156
158
 
157
159
  # NOTE in-process overlays first
158
160
 
159
161
  @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
160
163
 
161
- # XXX this is currently a single case of multi-process... process
162
- unless over_reel.duck
163
- raise Error, "Video overlays are not implemented just yet, sorry..." if over_reel.reel.channel?(:video)
164
-
165
- # Audio overlaying
164
+ fail Error, "Video overlays are not implemented just yet, sorry..." if over_reel.reel.channel?(:video)
166
165
 
167
- lbl_nxt = "oo#{i}"
166
+ # Audio overlaying
168
167
 
169
- lbl_over = "ol#{i}"
170
- filters +=
171
- over_reel.reel.filters_for(lbl_over, process: process) # NOTE audio only, see above
168
+ lbl_nxt = "oo#{i}"
172
169
 
173
- filters +=
174
- Filter.copy("#{lbl_out}:v", "#{lbl_nxt}:v") if channel?(:video)
175
- filters +=
176
- Filter.amix_to_first(["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a") if channel?(:audio)
177
-
178
- lbl_out = lbl_nxt
179
- end
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
173
+ )
174
+ filters.concat(
175
+ Filter.copy "#{lbl_out}:v", "#{lbl_nxt}:v"
176
+ ) if channel?(:video)
177
+ filters.concat(
178
+ Filter.amix_to_first_same_volume ["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a"
179
+ ) if channel?(:audio)
180
180
 
181
+ lbl_out = lbl_nxt
181
182
  end
182
183
 
183
184
  # NOTE multi-process overlays last
@@ -191,12 +192,12 @@ module Ffmprb
191
192
 
192
193
  # XXX this is currently a single case of multi-process... process
193
194
  if over_reel.duck
194
- raise Error, "Don't know how to duck video... yet" if over_reel.duck != :audio
195
+ fail Error, "Don't know how to duck video... yet" if over_reel.duck != :audio
195
196
 
196
197
  # So ducking just audio here, ye?
197
198
 
198
199
  main_a_o = channel_lbl_ios["#{lbl_out}:a"]
199
- raise Error, "Main output does not contain audio to duck" unless main_a_o
200
+ fail Error, "Main output does not contain audio to duck" unless main_a_o
200
201
  # XXX#181845 must really seperate channels for streaming (e.g. mp4 wouldn't stream through the fifo)
201
202
  main_a_inter_o = File.temp_fifo(main_a_o.extname)
202
203
  channel_lbl_ios.each do |channel_lbl, io|
@@ -204,27 +205,25 @@ module Ffmprb
204
205
  end
205
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"
206
207
 
207
- overlay_io = File.buffered_fifo(Process.intermediate_channel_extname :audio)
208
- process.threaded overlay_io.thr
208
+ overlay_i, overlay_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname :audio)
209
209
  lbl_over = "ol#{i}"
210
- filters +=
211
- over_reel.reel.filters_for(lbl_over, process: process, video: false, audio: true)
212
- channel_lbl_ios["#{lbl_over}:a"] = overlay_io.in
213
- Ffmprb.logger.debug "Routed and buffering an auxiliary output fifos (#{overlay_io.in.path}>#{overlay_io.out.path}) for overlay"
210
+ filters.concat(
211
+ over_reel.reel.filters_for lbl_over, process: process, output: self, video: false, audio: true
212
+ )
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"
214
215
 
215
- inter_io = File.buffered_fifo(main_a_inter_o.extname)
216
- process.threaded inter_io.thr
217
- Ffmprb.logger.debug "Allocated fifos to buffer media (#{inter_io.in.path}>#{inter_io.out.path}) while finding silence"
216
+ inter_i, inter_o = File.threaded_buffered_fifo(main_a_inter_o.extname)
217
+ Ffmprb.logger.debug "Allocated fifos to buffer media (#{inter_i.path}>#{inter_o.path}) while finding silence"
218
218
 
219
- thr = Util::Thread.new "audio ducking" do
220
- silence = Ffmprb.find_silence(main_a_inter_o, inter_io.in)
219
+ Util::Thread.new "audio ducking" do
220
+ silence = Ffmprb.find_silence(main_a_inter_o, inter_i)
221
221
 
222
222
  Ffmprb.logger.debug "Audio ducking with silence: [#{silence.map{|s| "#{s.start_at}-#{s.end_at}"}.join ', '}]"
223
223
 
224
- Process.duck_audio inter_io.out, overlay_io.out, silence, main_a_o,
224
+ Process.duck_audio inter_o, overlay_o, silence, main_a_o,
225
225
  video: (channel?(:video)? {resolution: target_resolution, fps: target_fps}: false)
226
226
  end
227
- process.threaded thr
228
227
  end
229
228
 
230
229
  end
@@ -238,6 +237,8 @@ module Ffmprb
238
237
  io_channel_lbls.each do |io, channel_lbls|
239
238
  channel_lbls.each do |channel_lbl|
240
239
  options << '-map' << "[#{channel_lbl}]"
240
+ # XXX temporary patchwork
241
+ options << '-c:a' << 'libmp3lame' if channel_lbl =~ /:a$/
241
242
  end
242
243
  options << io.path
243
244
  end
@@ -245,39 +246,36 @@ module Ffmprb
245
246
  end
246
247
  end
247
248
 
248
- def cut(
249
+ def roll(
250
+ reel,
251
+ onto: :full_screen,
249
252
  after: nil,
250
253
  transition: nil
251
254
  )
252
- raise Error, "Nothing to cut yet..." if @reels.empty? || @reels.last.reel.nil?
255
+ fail Error, "Nothing to roll..." unless reel
256
+ fail Error, "Supporting :transition with :after only at the moment, sorry." unless
257
+ !transition || after || @reels.to_a.empty?
253
258
 
254
- add_reel nil, after, transition, @reels.last.full_screen?
259
+ add_reel reel, after, transition, (onto == :full_screen)
255
260
  end
261
+ alias :lay :roll
256
262
 
257
263
  def overlay(
258
264
  reel,
259
265
  at: 0,
266
+ transition: nil,
260
267
  duck: nil
261
268
  )
262
- raise Error, "Nothing to overlay..." unless reel
263
- raise Error, "Nothing to lay over yet..." if @reels.to_a.empty?
264
- raise Error, "Ducking overlays should come last... for now" if !duck && @overlays.to_a.last && @overlays.to_a.last.duck
269
+ fail Error, "Nothing to overlay..." unless reel
270
+ fail Error, "Nothing to lay over yet..." if @reels.to_a.empty?
271
+ fail Error, "Ducking overlays should come last... for now" if !duck && @overlays.to_a.last && @overlays.to_a.last.duck
265
272
 
266
273
  (@overlays ||= []) <<
267
274
  OpenStruct.new(reel: reel, at: at, duck: duck)
268
275
  end
269
276
 
270
- def roll(
271
- reel,
272
- onto: :full_screen,
273
- after: nil,
274
- transition: nil
275
- )
276
- raise Error, "Nothing to roll..." unless reel
277
- raise Error, "Supporting :transition with :after only at the moment, sorry." unless
278
- !transition || after || @reels.to_a.empty?
279
-
280
- add_reel reel, after, transition, (onto == :full_screen)
277
+ def channel?(medium)
278
+ @channels.include?(medium) && @io.channel?(medium) && reels_channel?(medium)
281
279
  end
282
280
 
283
281
  def channel?(medium, force=false)
@@ -287,7 +285,7 @@ module Ffmprb
287
285
  reels_channel?(medium)
288
286
  end
289
287
 
290
- protected
288
+ # XXX TMP protected
291
289
 
292
290
  def resolve(io)
293
291
  return io unless io.is_a? String
@@ -295,22 +293,22 @@ module Ffmprb
295
293
  case io
296
294
  when /^\/\w/
297
295
  File.create(io).tap do |file|
298
- Ffmprb.logger.warn "Output file exists (#{file.path}), will overwrite" if file.exist?
296
+ Ffmprb.logger.warn "Output file exists (#{file.path}), will probably overwrite" if file.exist?
299
297
  end
300
298
  else
301
- raise Error, "Cannot resolve output: #{io}"
299
+ fail Error, "Cannot resolve output: #{io}"
302
300
  end
303
301
  end
304
302
 
305
- private
303
+ # XXX TMP private
306
304
 
307
305
  def reels_channel?(medium)
308
306
  @reels.to_a.all?{|r| !r.reel || r.reel.channel?(medium)}
309
307
  end
310
308
 
311
309
  def add_reel(reel, after, transition, full_screen)
312
- raise Error, "No time to roll..." if after && after.to_f <= 0
313
- raise Error, "Partial (not coming last in process) overlays are currently unsupported, sorry." unless @overlays.to_a.empty?
310
+ fail Error, "No time to roll..." if after && after.to_f <= 0
311
+ fail Error, "Partial (not coming last in process) overlays are currently unsupported, sorry." unless @overlays.to_a.empty?
314
312
 
315
313
  # NOTE limited functionality (see exception in Filter.transition_av): transition = {effect => duration}
316
314
  transition_length = transition.to_h.max_by{|k,v| v}.to_a.last.to_f
@@ -1,14 +1,13 @@
1
- require 'ffmprb/process/input'
2
- require 'ffmprb/process/output'
3
-
4
1
  module Ffmprb
5
2
 
6
3
  class Process
7
4
 
8
5
  class << self
9
6
 
10
- attr_accessor :duck_audio_hi, :duck_audio_lo
11
- attr_accessor :duck_audio_silent_min_sec, :duck_audio_transition_sec
7
+ attr_accessor :duck_audio_volume_hi, :duck_audio_volume_lo
8
+ attr_accessor :duck_audio_silent_min, :duck_audio_transition_length
9
+
10
+ attr_accessor :timeout
12
11
 
13
12
  def intermediate_channel_extname(*media)
14
13
  if media == [:video]
@@ -18,31 +17,38 @@ module Ffmprb
18
17
  elsif media.sort == [:audio, :video]
19
18
  '.flv'
20
19
  else
21
- raise Error, "I don't know how to channel [#{media.join ', '}]"
20
+ fail Error, "I don't know how to channel [#{media.join ', '}]"
22
21
  end
23
22
  end
24
23
 
25
- def duck_audio(av_main_i, a_overlay_i, silence, av_main_o, video: {resolution: Ffmprb::QVGA, fps: 30})
24
+ def duck_audio(av_main_i, a_overlay_i, silence, av_main_o,
25
+ volume_lo: duck_audio_volume_lo, volume_hi: duck_audio_volume_hi,
26
+ silent_min: duck_audio_silent_min, transition_length: duck_audio_transition_length,
27
+ video: {resolution: Ffmprb::CGA, fps: 30} # XXX temporary
28
+ )
26
29
  Ffmprb.process(av_main_i, a_overlay_i, silence, av_main_o) do |main_input, overlay_input, duck_data, main_output|
27
30
 
28
31
  in_main = input(main_input, **(video ? {} : {only: :audio}))
29
32
  in_over = input(overlay_input, only: :audio)
30
33
  output(main_output, **(video ? {resolution: video[:resolution], fps: video[:fps]} : {})) do
31
34
  roll in_main
32
- ducked_overlay_volume = {0.0 => Process.duck_audio_lo}
35
+
36
+ ducked_overlay_volume = {0.0 => volume_lo}
33
37
  duck_data.each do |silent|
34
- next if silent.end_at && silent.start_at && (silent.end_at - silent.start_at) < Process.duck_audio_silent_min_sec
38
+ next if silent.end_at && silent.start_at && (silent.end_at - silent.start_at) < silent_min
35
39
 
36
40
  ducked_overlay_volume.merge!(
37
- [silent.start_at - Process.duck_audio_transition_sec/2, 0.0].max => Process.duck_audio_lo,
38
- (silent.start_at + Process.duck_audio_transition_sec/2) => Process.duck_audio_hi
41
+ [silent.start_at - transition_length/2, 0.0].max => volume_lo,
42
+ (silent.start_at + transition_length/2) => volume_hi
39
43
  ) if silent.start_at
44
+
40
45
  ducked_overlay_volume.merge!(
41
- [silent.end_at - Process.duck_audio_transition_sec/2, 0.0].max => Process.duck_audio_hi,
42
- (silent.end_at + Process.duck_audio_transition_sec/2) => Process.duck_audio_lo
46
+ [silent.end_at - transition_length/2, 0.0].max => volume_hi,
47
+ (silent.end_at + transition_length/2) => volume_lo
43
48
  ) if silent.end_at
44
49
  end
45
50
  overlay in_over.volume ducked_overlay_volume
51
+
46
52
  Ffmprb.logger.debug "Ducking audio with volumes: {#{ducked_overlay_volume.map{|t,v| "#{t}: #{v}"}.join ', '}}"
47
53
  end
48
54
 
@@ -51,8 +57,11 @@ module Ffmprb
51
57
 
52
58
  end
53
59
 
54
- def initialize(*args, &blk)
60
+ attr_reader :timeout
61
+
62
+ def initialize(*args, **opts, &blk)
55
63
  @inputs = []
64
+ @timeout = opts[:timeout] || self.class.timeout
56
65
  end
57
66
 
58
67
  def input(io, only: nil)
@@ -61,17 +70,25 @@ module Ffmprb
61
70
  end
62
71
  end
63
72
 
64
- def output(io, only: nil, resolution: Ffmprb::QVGA, fps: 30, &blk)
65
- raise Error, "Just one output for now, sorry." if @output
73
+ def output(io, only: nil, resolution: Ffmprb::CGA, fps: 30, &blk)
74
+ fail Error, "Just one output for now, sorry." if @output
66
75
 
67
- @output = Output.new(io, only: only, resolution: resolution).tap do |out|
76
+ @output = Output.new(io, only: only, resolution: resolution, fps: fps).tap do |out|
68
77
  out.instance_exec &blk
69
78
  end
70
79
  end
71
80
 
72
- def run
73
- Util.ffmpeg *command
74
- @threaded.to_a.each &:join
81
+ # NOTE the one and the only entry-point processing function which spawns threads etc
82
+ def run(limit: nil) # (async: false)
83
+ # NOTE this is both for the future async: option and according to
84
+ # the threading policy (a parent death will be noticed and handled by children)
85
+ thr = Util::Thread.new do
86
+ # NOTE yes, an exception can occur anytime, and we'll just die, it's ok, see above
87
+ Util.ffmpeg(*command, limit: limit, timeout: timeout).tap do |res| # XXX just to return something
88
+ Util::Thread.join_children! limit, timeout: timeout
89
+ end
90
+ end
91
+ thr.value if thr.join limit # NOTE should not block for more than limit
75
92
  end
76
93
 
77
94
  def [](obj)
@@ -81,11 +98,6 @@ module Ffmprb
81
98
  end
82
99
  end
83
100
 
84
- # TODO deserves a better solution
85
- def threaded(thr)
86
- (@threaded ||= []) << thr
87
- end
88
-
89
101
  private
90
102
 
91
103
  def command
@@ -97,7 +109,7 @@ module Ffmprb
97
109
  end
98
110
 
99
111
  def output_options
100
- @output.options self
112
+ @output.options_for self
101
113
  end
102
114
 
103
115
  end
@@ -5,19 +5,101 @@ 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
9
+ class ParentError < Error; end
10
+
11
+ class << self
12
+
13
+ attr_accessor :timeout
14
+
15
+ def timeout_or_live(limit=nil, log: "while doing this", timeout: self.timeout, &blk)
16
+ started_at = Time.now
17
+ tries = 0
18
+ logged_tries = 0
19
+ begin
20
+ tries += 1
21
+ time = Time.now - started_at
22
+ fail TimeLimitError if limit && time > limit
23
+ Timeout.timeout timeout do
24
+ blk.call time
25
+ end
26
+ rescue Timeout::Error
27
+ if tries > 2 * logged_tries
28
+ Ffmprb.logger.info "A little bit of timeout #{log.respond_to?(:call)? log.call : log} (##{tries})"
29
+ logged_tries = tries
30
+ end
31
+ current.live!
32
+ retry
33
+ end
34
+ end
35
+
36
+ def join_children!(limit=nil, timeout: self.timeout)
37
+ Thread.current.join_children! limit, timeout: timeout
38
+ end
39
+
40
+ end
41
+
42
+ attr_reader :name
43
+
8
44
  def initialize(name="some", &blk)
45
+ @name = name
46
+ @parent = Thread.current
47
+ @live_children = []
48
+ @children_mon = Monitor.new
49
+ @dead_children_q = Queue.new
50
+ Ffmprb.logger.debug "about to launch #{name}"
51
+ sync_q = Queue.new
9
52
  super() do
53
+ @parent.child_lives self if @parent.respond_to? :child_lives
54
+ sync_q.enq :ok
55
+ Ffmprb.logger.debug "#{name} thread launched"
10
56
  begin
11
- Ffmprb.logger.debug "#{name} thread launched"
12
- blk.call
13
- Ffmprb.logger.debug "#{name} thread done"
57
+ blk.call.tap do
58
+ Ffmprb.logger.debug "#{name} thread done"
59
+ end
14
60
  rescue Exception
15
- Ffmprb.logger.warn "#{$!.class} caught in a #{name} thread (hidden): #{$!.message}\nBacktrace:\n\t#{$!.backtrace.join("\n\t")}"
61
+ Ffmprb.logger.warn "#{$!.class} raised in #{name} thread: #{$!.message}\nBacktrace:\n\t#{$!.backtrace.join("\n\t")}"
16
62
  cause = $!
17
63
  Ffmprb.logger.warn "...caused by #{cause.class}: #{cause.message}\nBacktrace:\n\t#{cause.backtrace.join("\n\t")}" while
18
64
  cause = cause.cause
19
- raise
65
+ fail $! # XXX I have no idea why I need to give it `$!` -- the docs say I need not
66
+ ensure
67
+ @parent.child_dies self if @parent.respond_to? :child_dies
68
+ end
69
+ end
70
+ sync_q.deq
71
+ end
72
+
73
+ # XXX protected: none of these methods should be called by a user code, the only public methods are above
74
+
75
+ def live!
76
+ fail ParentError if @parent.status.nil?
77
+ end
78
+
79
+ def child_lives(thr)
80
+ @children_mon.synchronize do
81
+ Ffmprb.logger.debug "picking up #{thr.name} thread"
82
+ @live_children << thr
83
+ end
84
+ end
85
+
86
+ def child_dies(thr)
87
+ @children_mon.synchronize do
88
+ Ffmprb.logger.debug "releasing #{thr.name} thread"
89
+ @dead_children_q.enq thr
90
+ fail "System Error" unless @live_children.delete thr
91
+ end
92
+ end
93
+
94
+ def join_children!(limit=nil, timeout: self.class.timeout)
95
+ timeout = [timeout, limit].compact.min
96
+ Ffmprb.logger.debug "joining threads: #{@live_children.size} live, #{@dead_children_q.size} dead"
97
+ until @live_children.empty? && @dead_children_q.empty?
98
+ thr = self.class.timeout_or_live limit, log: "joining threads: #{@live_children.size} live, #{@dead_children_q.size} dead", timeout: timeout do
99
+ @dead_children_q.deq
20
100
  end
101
+ Ffmprb.logger.debug "joining the late #{thr.name} thread"
102
+ fail "System Error" unless thr.join(timeout) # NOTE should not block
21
103
  end
22
104
  end
23
105