ffmprb 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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