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.
- 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
|
|