ffmprb 0.7.0 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Guardfile +1 -1
- data/README.md +4 -2
- data/circle.yml +5 -1
- data/lib/defaults.rb +21 -0
- data/lib/ffmprb/file/sample.rb +48 -0
- data/lib/ffmprb/file.rb +17 -50
- data/lib/ffmprb/filter.rb +46 -28
- data/lib/ffmprb/find_silence.rb +30 -19
- data/lib/ffmprb/process/input.rb +48 -20
- data/lib/ffmprb/process/output.rb +109 -111
- data/lib/ffmprb/process.rb +38 -26
- data/lib/ffmprb/util/thread.rb +87 -5
- data/lib/ffmprb/util/{io_buffer.rb → threaded_io_buffer.rb} +90 -110
- data/lib/ffmprb/util.rb +62 -31
- data/lib/ffmprb/version.rb +1 -1
- data/lib/ffmprb.rb +10 -24
- metadata +5 -3
@@ -4,7 +4,7 @@ module Ffmprb
|
|
4
4
|
|
5
5
|
class Output
|
6
6
|
|
7
|
-
def initialize(io, only
|
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
|
18
|
-
|
19
|
-
|
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
|
43
|
-
|
44
|
-
filters
|
45
|
-
Filter.scale_pad_fps
|
46
|
-
|
47
|
-
filters
|
48
|
-
Filter.anull
|
49
|
-
|
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.
|
64
|
-
|
65
|
-
filters
|
66
|
-
Filter.silent_source
|
67
|
-
|
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
|
74
|
-
|
75
|
-
filters
|
76
|
-
Filter.concat_a
|
77
|
-
|
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
|
87
|
-
|
88
|
-
filters
|
89
|
-
Filter.asplit
|
90
|
-
|
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
|
105
|
-
|
106
|
-
filters
|
107
|
-
Filter.atrim
|
108
|
-
|
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.
|
124
|
-
|
125
|
-
filters
|
126
|
-
Filter.silent_source
|
127
|
-
|
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
|
131
|
-
|
132
|
-
filters
|
133
|
-
Filter.atrim
|
134
|
-
|
135
|
-
filters
|
136
|
-
Filter.transition_av
|
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
|
152
|
-
|
153
|
-
|
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
|
-
|
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
|
-
|
166
|
+
# Audio overlaying
|
168
167
|
|
169
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
lbl_out
|
179
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
212
|
-
|
213
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
220
|
-
silence = Ffmprb.find_silence(main_a_inter_o,
|
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
|
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
|
249
|
+
def roll(
|
250
|
+
reel,
|
251
|
+
onto: :full_screen,
|
249
252
|
after: nil,
|
250
253
|
transition: nil
|
251
254
|
)
|
252
|
-
|
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
|
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
|
-
|
263
|
-
|
264
|
-
|
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
|
271
|
-
|
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
|
-
|
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
|
-
|
313
|
-
|
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
|
data/lib/ffmprb/process.rb
CHANGED
@@ -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 :
|
11
|
-
attr_accessor :
|
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
|
-
|
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,
|
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
|
-
|
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) <
|
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 -
|
38
|
-
(silent.start_at +
|
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 -
|
42
|
-
(silent.end_at +
|
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
|
-
|
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::
|
65
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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.
|
112
|
+
@output.options_for self
|
101
113
|
end
|
102
114
|
|
103
115
|
end
|
data/lib/ffmprb/util/thread.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
13
|
-
|
57
|
+
blk.call.tap do
|
58
|
+
Ffmprb.logger.debug "#{name} thread done"
|
59
|
+
end
|
14
60
|
rescue Exception
|
15
|
-
Ffmprb.logger.warn "#{$!.class}
|
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
|
-
|
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
|
|