ffmprb 0.11.3 → 0.11.4
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/Dockerfile +12 -0
- data/Gemfile +8 -1
- data/Gemfile.lock +121 -0
- data/README.md +10 -8
- data/TODO.md +1 -0
- data/bin/test +7 -0
- data/coverage/assets/0.10.0/application.css +799 -0
- data/coverage/assets/0.10.0/application.js +1707 -0
- data/coverage/assets/0.10.0/colorbox/border.png +0 -0
- data/coverage/assets/0.10.0/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.0/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.0/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.0/favicon_green.png +0 -0
- data/coverage/assets/0.10.0/favicon_red.png +0 -0
- data/coverage/assets/0.10.0/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.0/loading.gif +0 -0
- data/coverage/assets/0.10.0/magnify.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.10.2/application.css +799 -0
- data/coverage/assets/0.10.2/application.js +1707 -0
- data/coverage/assets/0.10.2/colorbox/border.png +0 -0
- data/coverage/assets/0.10.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.2/favicon_green.png +0 -0
- data/coverage/assets/0.10.2/favicon_red.png +0 -0
- data/coverage/assets/0.10.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.2/loading.gif +0 -0
- data/coverage/assets/0.10.2/magnify.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_both.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
- data/coverage/assets/0.12.2/application.css +1 -0
- data/coverage/assets/0.12.2/application.js +7 -0
- data/coverage/assets/0.12.2/colorbox/border.png +0 -0
- data/coverage/assets/0.12.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.12.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.12.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.12.2/favicon_green.png +0 -0
- data/coverage/assets/0.12.2/favicon_red.png +0 -0
- data/coverage/assets/0.12.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.12.2/loading.gif +0 -0
- data/coverage/assets/0.12.2/magnify.png +0 -0
- data/coverage/index.html +24412 -0
- data/ffmprb.gemspec +34 -14
- data/lib/defaults.rb +1 -1
- data/lib/ffmprb.rb +3 -3
- data/lib/ffmprb/file.rb +8 -8
- data/lib/ffmprb/file/sample.rb +2 -2
- data/lib/ffmprb/file/threaded_buffered.rb +3 -3
- data/lib/ffmprb/filter.rb +4 -1
- data/lib/ffmprb/find_silence.rb +5 -2
- data/lib/ffmprb/process.rb +5 -3
- data/lib/ffmprb/process/input.rb +1 -1
- data/lib/ffmprb/process/input/looping.rb +6 -11
- data/lib/ffmprb/process/output.rb +9 -6
- data/lib/ffmprb/util.rb +5 -3
- data/lib/ffmprb/util/proc_vis.rb +1 -1
- data/lib/ffmprb/util/thread.rb +6 -5
- data/lib/ffmprb/util/threaded_io_buffer.rb +20 -19
- data/lib/ffmprb/version.rb +2 -2
- data/tmp/output.rb +383 -0
- metadata +90 -138
- data/.gitignore +0 -10
- data/.rspec +0 -4
- data/.ruby-version +0 -1
- data/.travis.yml +0 -3
- data/circle.yml +0 -7
|
@@ -29,7 +29,7 @@ module Ffmprb
|
|
|
29
29
|
def initialize(input, *outputs, keep_outputs_open_on_input_idle_limit: nil)
|
|
30
30
|
super() # NOTE for the monitor, apparently
|
|
31
31
|
|
|
32
|
-
Ffmprb.logger.debug
|
|
32
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer initializing with (#{ThreadedIoBuffer.blocks_max}x#{ThreadedIoBuffer.block_size})"}
|
|
33
33
|
|
|
34
34
|
@input = input
|
|
35
35
|
@outputs = outputs.map do |outp|
|
|
@@ -47,10 +47,11 @@ module Ffmprb
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
Thread.join_children!.tap do
|
|
50
|
-
Ffmprb.logger.debug
|
|
50
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer (#{@input.path}->#{@outputs.map(&:io).map(&:path)}) terminated successfully (#{stats})"}
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
|
+
# TODO?
|
|
54
55
|
#
|
|
55
56
|
# def once(event, &blk)
|
|
56
57
|
# event = event.to_sym
|
|
@@ -58,17 +59,17 @@ module Ffmprb
|
|
|
58
59
|
# if @events[event].respond_to? :call
|
|
59
60
|
# fail Error, "Once upon a time (one #once(event) at a time) please"
|
|
60
61
|
# elsif @events[event]
|
|
61
|
-
# Ffmprb.logger.debug
|
|
62
|
+
# Ffmprb.logger.debug{"ThreadedIoBuffer (post-)reacting to #{event}"}
|
|
62
63
|
# @handler_thr = Util::Thread.new "#{event} handler", &blk
|
|
63
64
|
# else
|
|
64
|
-
# Ffmprb.logger.debug
|
|
65
|
+
# Ffmprb.logger.debug{"ThreadedIoBuffer subscribing to #{event}"}
|
|
65
66
|
# @events[event] = blk
|
|
66
67
|
# end
|
|
67
68
|
# end
|
|
68
69
|
# handle_synchronously :once
|
|
69
70
|
#
|
|
70
71
|
# def reader_done!
|
|
71
|
-
# Ffmprb.logger.debug
|
|
72
|
+
# Ffmprb.logger.debug{"ThreadedIoBuffer reader terminated (#{stats})"}
|
|
72
73
|
# fire! :reader_done
|
|
73
74
|
# end
|
|
74
75
|
#
|
|
@@ -84,7 +85,7 @@ module Ffmprb
|
|
|
84
85
|
#
|
|
85
86
|
# def fire!(event)
|
|
86
87
|
# wait_for_handler!
|
|
87
|
-
# Ffmprb.logger.debug
|
|
88
|
+
# Ffmprb.logger.debug{"ThreadedIoBuffer firing #{event}"}
|
|
88
89
|
# if blk = @events.to_h[event.to_sym]
|
|
89
90
|
# @handler_thr = Util::Thread.new "#{event} handler", &blk
|
|
90
91
|
# end
|
|
@@ -104,9 +105,9 @@ module Ffmprb
|
|
|
104
105
|
|
|
105
106
|
def reader_input! # NOTE just for reader thread
|
|
106
107
|
if @input.respond_to?(:call)
|
|
107
|
-
Ffmprb.logger.debug
|
|
108
|
+
Ffmprb.logger.debug{"Opening buffer input"}
|
|
108
109
|
@input = @input.call
|
|
109
|
-
Ffmprb.logger.debug
|
|
110
|
+
Ffmprb.logger.debug{"Opened buffer input: #{@input.path}"}
|
|
110
111
|
end
|
|
111
112
|
@input
|
|
112
113
|
end
|
|
@@ -137,17 +138,17 @@ module Ffmprb
|
|
|
137
138
|
rescue IO::WaitReadable
|
|
138
139
|
if @keep_outputs_open_on_input_idle_limit && stats.bytes_in > 0 && stats.blocks_buff == 0 && timeouts * ThreadedIoBuffer.io_wait_timeout > @keep_outputs_open_on_input_idle_limit
|
|
139
140
|
if s.length > 0 # NOTE let's see if it helps outputting an incomplete block
|
|
140
|
-
Ffmprb.logger.debug
|
|
141
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) giving a chance to write #{s.length}/#{ThreadedIoBuffer.block_size}b after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b"}
|
|
141
142
|
break
|
|
142
143
|
else
|
|
143
|
-
Ffmprb.logger.debug
|
|
144
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) giving up after waiting >#{@keep_outputs_open_on_input_idle_limit}s, after reading #{stats.bytes_in}b, closing outputs"}
|
|
144
145
|
raise EOFError
|
|
145
146
|
end
|
|
146
147
|
else
|
|
147
148
|
Thread.current.live!
|
|
148
149
|
timeouts += 1
|
|
149
150
|
if timeouts > 2 * logged_timeouts
|
|
150
|
-
Ffmprb.logger.debug
|
|
151
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) retrying... (#{timeouts} reads): #{$!.class}"}
|
|
151
152
|
logged_timeouts = timeouts
|
|
152
153
|
end
|
|
153
154
|
IO.select [input_io], nil, nil, ThreadedIoBuffer.io_wait_timeout
|
|
@@ -165,7 +166,7 @@ module Ffmprb
|
|
|
165
166
|
output_enq! s
|
|
166
167
|
end
|
|
167
168
|
rescue EOFError
|
|
168
|
-
Ffmprb.logger.debug
|
|
169
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer reader (from #{input_io.path}) breaking off"}
|
|
169
170
|
rescue AllOutputsBrokenError
|
|
170
171
|
Ffmprb.logger.info "All outputs broken"
|
|
171
172
|
rescue Exception
|
|
@@ -182,7 +183,7 @@ module Ffmprb
|
|
|
182
183
|
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer input: #{$!.message}"
|
|
183
184
|
end
|
|
184
185
|
# reader_done!
|
|
185
|
-
Ffmprb.logger.debug
|
|
186
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer reader terminated (#{stats})"}
|
|
186
187
|
end
|
|
187
188
|
end
|
|
188
189
|
end
|
|
@@ -191,13 +192,13 @@ module Ffmprb
|
|
|
191
192
|
return output.io = output._io unless output._io.respond_to?(:call)
|
|
192
193
|
|
|
193
194
|
output.thr = Thread.new("buffer writer output helper") do
|
|
194
|
-
Ffmprb.logger.debug
|
|
195
|
+
Ffmprb.logger.debug{"Opening buffer output"}
|
|
195
196
|
output.io =
|
|
196
197
|
Thread.timeout_or_live nil, log: "in the buffer writer helper thread", timeout: ThreadedIoBuffer.timeout do |time|
|
|
197
198
|
fail Error, "giving up buffer writer init since the reader has failed (#{@reader_failed.message})" if @reader_failed
|
|
198
199
|
output._io.call
|
|
199
200
|
end
|
|
200
|
-
Ffmprb.logger.debug
|
|
201
|
+
Ffmprb.logger.debug{"Opened buffer output: #{output.io.path}"}
|
|
201
202
|
end
|
|
202
203
|
end
|
|
203
204
|
|
|
@@ -223,7 +224,7 @@ module Ffmprb
|
|
|
223
224
|
Thread.current.live!
|
|
224
225
|
timeouts += 1
|
|
225
226
|
if timeouts > 2 * logged_timeouts
|
|
226
|
-
Ffmprb.logger.debug
|
|
227
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) retrying... (#{timeouts} writes): #{$!.class}"}
|
|
227
228
|
logged_timeouts = timeouts
|
|
228
229
|
end
|
|
229
230
|
IO.select nil, [output_io], nil, ThreadedIoBuffer.io_wait_timeout
|
|
@@ -234,9 +235,9 @@ module Ffmprb
|
|
|
234
235
|
retry
|
|
235
236
|
end
|
|
236
237
|
end
|
|
237
|
-
Ffmprb.logger.debug
|
|
238
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) breaking off"}
|
|
238
239
|
rescue Errno::EPIPE
|
|
239
|
-
Ffmprb.logger.debug
|
|
240
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io.path}) broken"}
|
|
240
241
|
output.broken = true
|
|
241
242
|
ensure
|
|
242
243
|
# terminated!
|
|
@@ -246,7 +247,7 @@ module Ffmprb
|
|
|
246
247
|
Ffmprb.logger.error "#{$!.class.name} closing ThreadedIoBuffer output: #{$!.message}"
|
|
247
248
|
end
|
|
248
249
|
output.broken = true
|
|
249
|
-
Ffmprb.logger.debug
|
|
250
|
+
Ffmprb.logger.debug{"ThreadedIoBuffer writer (to #{output_io && output_io.path}) terminated (#{stats})"}
|
|
250
251
|
end
|
|
251
252
|
end
|
|
252
253
|
end
|
data/lib/ffmprb/version.rb
CHANGED
data/tmp/output.rb
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
module Ffmprb
|
|
2
|
+
|
|
3
|
+
class Process
|
|
4
|
+
|
|
5
|
+
class Output
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
|
|
9
|
+
def video_args(video=nil)
|
|
10
|
+
video = Process.output_video_options.merge(video.to_h)
|
|
11
|
+
[].tap do |args|
|
|
12
|
+
encoder = pixel_format = nil # NOTE ah, ruby
|
|
13
|
+
args.concat %W[-c:v #{encoder}] if (encoder = video.delete(:encoder))
|
|
14
|
+
args.concat %W[-pix_fmt #{pixel_format}] if (pixel_format = video.delete(:pixel_format))
|
|
15
|
+
video.delete :resolution # NOTE is handled otherwise
|
|
16
|
+
video.delete :fps # NOTE is handled otherwise
|
|
17
|
+
Util.assert_options_empty! video
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def audio_args(audio=nil)
|
|
22
|
+
audio = Process.output_audio_options.merge(audio.to_h)
|
|
23
|
+
[].tap do |args|
|
|
24
|
+
encoder = nil
|
|
25
|
+
args.concat %W[-c:a #{encoder}] if (encoder = audio.delete(:encoder))
|
|
26
|
+
args.concat %W[-ar #{sampling_freq}] if (sampling_freq = audio.delete(:sampling_freq))
|
|
27
|
+
Util.assert_options_empty! audio
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve(io)
|
|
32
|
+
return io unless io.is_a? String # XXX ::File
|
|
33
|
+
|
|
34
|
+
File.create(io).tap do |file|
|
|
35
|
+
Ffmprb.logger.warn "Output file exists (#{file.path}), will probably overwrite" if file.exist?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :io
|
|
42
|
+
attr_reader :process
|
|
43
|
+
|
|
44
|
+
def initialize(io, process, video:, audio:)
|
|
45
|
+
@io = self.class.resolve(io)
|
|
46
|
+
@process = process
|
|
47
|
+
@channels = {
|
|
48
|
+
video: video && @io.channel?(:video) && OpenStruct.new(video),
|
|
49
|
+
audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
|
|
50
|
+
}
|
|
51
|
+
@lays = []
|
|
52
|
+
@overlays = []
|
|
53
|
+
@overducks = []
|
|
54
|
+
|
|
55
|
+
if channel?(:video)
|
|
56
|
+
channel(:video).resolution.to_s.split('x').each do |dim|
|
|
57
|
+
fail Error, "Both dimensions of a resolution must be divisible by 2, sorry about that" unless dim.to_i % 2 == 0
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# XXX This method is exceptionally long at the moment. This is not too grand.
|
|
63
|
+
# However, structuring the code should be undertaken with care, as not to harm the composition clarity.
|
|
64
|
+
def filters
|
|
65
|
+
if @filters
|
|
66
|
+
Ffmprb.logger.warn "Output filters called for the second time, disregarding any changes to #{self}. If you're an end user, this calls for a bug report, if you're a dev, welcome to Debugland."
|
|
67
|
+
return @filters
|
|
68
|
+
end
|
|
69
|
+
fail Error, "Nothing to roll..." if @lays.empty?
|
|
70
|
+
|
|
71
|
+
idx = process.output_index(self)
|
|
72
|
+
|
|
73
|
+
@filters = []
|
|
74
|
+
|
|
75
|
+
# Concatting
|
|
76
|
+
segments = []
|
|
77
|
+
|
|
78
|
+
@lays.each_with_index do |curr_lay, i|
|
|
79
|
+
|
|
80
|
+
lbl = nil
|
|
81
|
+
|
|
82
|
+
if curr_lay.reel
|
|
83
|
+
|
|
84
|
+
# NOTE mapping input to this lbl
|
|
85
|
+
|
|
86
|
+
lbl = "o#{idx}rl#{i}"
|
|
87
|
+
|
|
88
|
+
# NOTE Image-Padding to match the target resolution
|
|
89
|
+
# TODO full screen only at the moment (see exception above)
|
|
90
|
+
|
|
91
|
+
@filters.concat(
|
|
92
|
+
curr_lay.reel.filters_for lbl, video: channel(:video), audio: channel(:audio)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
trim_prev_at = curr_lay.after || (curr_lay.transition && 0)
|
|
97
|
+
transition_length = curr_lay.transition ? curr_lay.transition.length : 0
|
|
98
|
+
|
|
99
|
+
if trim_prev_at
|
|
100
|
+
|
|
101
|
+
# NOTE make sure previous lay rolls _long_ enough AND then _just_ enough
|
|
102
|
+
|
|
103
|
+
prev_lbl = segments.pop
|
|
104
|
+
|
|
105
|
+
lbl_pad = "bl#{prev_lbl}#{i}"
|
|
106
|
+
# NOTE generously padding the previous segment to support for all the cases
|
|
107
|
+
@filters.concat(
|
|
108
|
+
Filter.blank_source trim_prev_at + transition_length, channel(:video).resolution, channel(:video).fps, "#{lbl_pad}:v"
|
|
109
|
+
) if channel?(:video)
|
|
110
|
+
@filters.concat(
|
|
111
|
+
Filter.silent_source trim_prev_at + transition_length, "#{lbl_pad}:a"
|
|
112
|
+
) if channel?(:audio)
|
|
113
|
+
|
|
114
|
+
if prev_lbl
|
|
115
|
+
lbl_aux = lbl_pad
|
|
116
|
+
lbl_pad = "pd#{prev_lbl}#{i}"
|
|
117
|
+
@filters.concat(
|
|
118
|
+
Filter.concat_v ["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v"
|
|
119
|
+
) if channel?(:video)
|
|
120
|
+
@filters.concat(
|
|
121
|
+
Filter.concat_a ["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a"
|
|
122
|
+
) if channel?(:audio)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if curr_lay.transition
|
|
126
|
+
|
|
127
|
+
# NOTE Split the previous segment for transition
|
|
128
|
+
|
|
129
|
+
if trim_prev_at > 0
|
|
130
|
+
@filters.concat(
|
|
131
|
+
Filter.split "#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]
|
|
132
|
+
) if channel?(:video)
|
|
133
|
+
@filters.concat(
|
|
134
|
+
Filter.asplit "#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]
|
|
135
|
+
) if channel?(:audio)
|
|
136
|
+
lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
|
|
137
|
+
else
|
|
138
|
+
lbl_pad, lbl_pad_ = nil, lbl_pad
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if lbl_pad
|
|
143
|
+
|
|
144
|
+
# NOTE Trim the previous segment finally
|
|
145
|
+
|
|
146
|
+
new_prev_lbl = "tm#{prev_lbl}#{i}a"
|
|
147
|
+
|
|
148
|
+
@filters.concat(
|
|
149
|
+
Filter.trim 0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v"
|
|
150
|
+
) if channel?(:video)
|
|
151
|
+
@filters.concat(
|
|
152
|
+
Filter.atrim 0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a"
|
|
153
|
+
) if channel?(:audio)
|
|
154
|
+
|
|
155
|
+
segments << new_prev_lbl
|
|
156
|
+
Ffmprb.logger.debug{"Concatting segments: #{new_prev_lbl} pushed"}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if curr_lay.transition
|
|
160
|
+
|
|
161
|
+
# NOTE snip the end of the previous segment and combine with this lay
|
|
162
|
+
|
|
163
|
+
lbl_end1 = "o#{idx}tm#{i}b"
|
|
164
|
+
lbl_lay = "o#{idx}tn#{i}"
|
|
165
|
+
|
|
166
|
+
if !lbl # no lay
|
|
167
|
+
lbl_aux = "o#{idx}bk#{i}"
|
|
168
|
+
@filters.concat(
|
|
169
|
+
Filter.blank_source transition_length, channel(:video).resolution, channel(:video).fps, "#{lbl_aux}:v"
|
|
170
|
+
) if channel?(:video)
|
|
171
|
+
@filters.concat(
|
|
172
|
+
Filter.silent_source transition_length, "#{lbl_aux}:a"
|
|
173
|
+
) if channel?(:audio)
|
|
174
|
+
end # NOTE else hope lbl is long enough for the transition
|
|
175
|
+
|
|
176
|
+
@filters.concat(
|
|
177
|
+
Filter.trim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
|
|
178
|
+
) if channel?(:video)
|
|
179
|
+
@filters.concat(
|
|
180
|
+
Filter.atrim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a"
|
|
181
|
+
) if channel?(:audio)
|
|
182
|
+
|
|
183
|
+
# TODO the only supported transition, see #*lay
|
|
184
|
+
@filters.concat(
|
|
185
|
+
Filter.blend_v transition_length, channel(:video).resolution, channel(:video).fps, ["#{lbl_end1}:v", "#{lbl || lbl_aux}:v"], "#{lbl_lay}:v"
|
|
186
|
+
) if channel?(:video)
|
|
187
|
+
@filters.concat(
|
|
188
|
+
Filter.blend_a transition_length, ["#{lbl_end1}:a", "#{lbl || lbl_aux}:a"], "#{lbl_lay}:a"
|
|
189
|
+
) if channel?(:audio)
|
|
190
|
+
|
|
191
|
+
lbl = lbl_lay
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
segments << lbl # NOTE can be nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
segments.compact!
|
|
200
|
+
|
|
201
|
+
lbl_out = segments[0]
|
|
202
|
+
|
|
203
|
+
if segments.size > 1
|
|
204
|
+
lbl_out = "o#{idx}o"
|
|
205
|
+
|
|
206
|
+
@filters.concat(
|
|
207
|
+
Filter.concat_v segments.map{|s| "#{s}:v"}, "#{lbl_out}:v"
|
|
208
|
+
) if channel?(:video)
|
|
209
|
+
@filters.concat(
|
|
210
|
+
Filter.concat_a segments.map{|s| "#{s}:a"}, "#{lbl_out}:a"
|
|
211
|
+
) if channel?(:audio)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Overlays
|
|
215
|
+
|
|
216
|
+
# NOTE in-process overlays first
|
|
217
|
+
|
|
218
|
+
@overlays.each_with_index do |over_lay, i|
|
|
219
|
+
|
|
220
|
+
# Audio overlaying
|
|
221
|
+
|
|
222
|
+
lbl_nxt = "o#{idx}o#{i}"
|
|
223
|
+
|
|
224
|
+
lbl_over = "o#{idx}l#{i}"
|
|
225
|
+
@filters.concat( # NOTE audio only, see above
|
|
226
|
+
over_lay.reel.filters_for lbl_over, video: false, audio: channel(:audio)
|
|
227
|
+
)
|
|
228
|
+
@filters.concat(
|
|
229
|
+
Filter.copy "#{lbl_out}:v", "#{lbl_nxt}:v"
|
|
230
|
+
) if channel?(:video)
|
|
231
|
+
@filters.concat(
|
|
232
|
+
Filter.amix_to_first_same_volume ["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a"
|
|
233
|
+
) if channel?(:audio)
|
|
234
|
+
|
|
235
|
+
lbl_out = lbl_nxt
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# NOTE multi-process overlays last
|
|
239
|
+
|
|
240
|
+
@channel_lbl_ios = {} # XXX this is a spaghetti machine
|
|
241
|
+
@channel_lbl_ios["#{lbl_out}:v"] = io if channel?(:video)
|
|
242
|
+
@channel_lbl_ios["#{lbl_out}:a"] = io if channel?(:audio)
|
|
243
|
+
|
|
244
|
+
@overducks.each_with_index do |over_duck, i|
|
|
245
|
+
# NOTE just audio for now
|
|
246
|
+
|
|
247
|
+
Ffmprb.logger.info "ATTENTION: ducking audio (due to the absence of a simple ffmpeg filter) does not support streaming main input. yet."
|
|
248
|
+
|
|
249
|
+
main_av_o = @channel_lbl_ios["#{lbl_out}:a"]
|
|
250
|
+
fail Error, "Main output does not contain audio to duck" unless main_av_o
|
|
251
|
+
|
|
252
|
+
intermediate_extname = Process.intermediate_channel_extname video: main_av_o.channel?(:video), audio: main_av_o.channel?(:audio)
|
|
253
|
+
main_av_inter_i, main_av_inter_o = File.threaded_buffered_fifo(intermediate_extname, reader_open_on_writer_idle_limit: Util::ThreadedIoBuffer.timeout * 2, proc_vis: process)
|
|
254
|
+
@channel_lbl_ios.each do |channel_lbl, io|
|
|
255
|
+
@channel_lbl_ios[channel_lbl] = main_av_inter_i if io == main_av_o # XXX ~~~spaghetti
|
|
256
|
+
end
|
|
257
|
+
process.proc_vis_edge process, main_av_o, :remove
|
|
258
|
+
process.proc_vis_edge process, main_av_inter_i
|
|
259
|
+
Ffmprb.logger.debug{"Re-routed the main audio output (#{main_av_inter_i.path}->...->#{main_av_o.path}) through the process of audio ducking"}
|
|
260
|
+
|
|
261
|
+
over_a_i, over_a_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname(audio: true, video: false), proc_vis: process)
|
|
262
|
+
lbl_over = "o#{idx}d#{i}"
|
|
263
|
+
@filters.concat(
|
|
264
|
+
over_duck.reel.filters_for lbl_over, video: false, audio: channel(:audio)
|
|
265
|
+
)
|
|
266
|
+
@channel_lbl_ios["#{lbl_over}:a"] = over_a_i
|
|
267
|
+
process.proc_vis_edge process, over_a_i
|
|
268
|
+
Ffmprb.logger.debug{"Routed and buffering auxiliary output fifos (#{over_a_i.path}>#{over_a_o.path}) for overlay"}
|
|
269
|
+
|
|
270
|
+
inter_i, inter_o = File.threaded_buffered_fifo(intermediate_extname, proc_vis: process)
|
|
271
|
+
Ffmprb.logger.debug{"Allocated fifos to buffer media (#{inter_i.path}>#{inter_o.path}) while finding silence"}
|
|
272
|
+
|
|
273
|
+
ignore_broken_pipes_was = process.ignore_broken_pipes
|
|
274
|
+
process.ignore_broken_pipes = true # NOTE audio ducking process may break the overlay pipe
|
|
275
|
+
|
|
276
|
+
Util::Thread.new "audio ducking" do
|
|
277
|
+
process.proc_vis_edge main_av_inter_o, inter_i # XXX mark it better
|
|
278
|
+
silence = Ffmprb.find_silence(main_av_inter_o, inter_i)
|
|
279
|
+
|
|
280
|
+
silence_res = silence.map{|s| "#{s.start_at}-#{s.end_at}"}.join(', ')
|
|
281
|
+
Ffmprb.logger.debug{"Audio ducking with silence: [#{silence_res}]"}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# NOTE (not) ignoring broken pipes here is hopefully what the caller intended
|
|
285
|
+
Process.duck_audio inter_o, over_a_o, silence, main_av_o,
|
|
286
|
+
process_options: {parent: process, ignore_broken_pipes: ignore_broken_pipes_was, timeout: process.timeout},
|
|
287
|
+
video: channel(:video), audio: channel(:audio)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@filters
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def args
|
|
296
|
+
fail Error, "Must generate filters first." unless @channel_lbl_ios
|
|
297
|
+
|
|
298
|
+
[].tap do |args|
|
|
299
|
+
io_channel_lbls = {} # XXX ~~~spaghetti
|
|
300
|
+
@channel_lbl_ios.each do |channel_lbl, io|
|
|
301
|
+
(io_channel_lbls[io] ||= []) << channel_lbl
|
|
302
|
+
end
|
|
303
|
+
io_channel_lbls.each do |io, channel_lbls|
|
|
304
|
+
channel_lbls.each do |channel_lbl|
|
|
305
|
+
args.concat ['-map', "[#{channel_lbl}]"]
|
|
306
|
+
end
|
|
307
|
+
args.concat self.class.video_args(channel :video) if channel? :video
|
|
308
|
+
args.concat self.class.audio_args(channel :audio) if channel? :audio
|
|
309
|
+
args << io.path
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def input(io, video: true, audio: true)
|
|
315
|
+
process.input io, video: video, audio: audio
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def roll(
|
|
319
|
+
reel,
|
|
320
|
+
after: nil,
|
|
321
|
+
transition: nil
|
|
322
|
+
)
|
|
323
|
+
fail Error, "No reel" unless reel
|
|
324
|
+
fail Error, "No time to roll (after: #{after})..." if after && after.to_f <= 0
|
|
325
|
+
fail Error, "Supporting :transition with :after only at the moment, sorry." unless
|
|
326
|
+
!transition || after || @lays.empty?
|
|
327
|
+
Ffmprb.logger.warn "Overlays are over all the lays, so they'd better be after all the lays in the script." unless @overlays.empty? && @overducks.empty?
|
|
328
|
+
|
|
329
|
+
add_lay reel, after.to_f, transition
|
|
330
|
+
end
|
|
331
|
+
alias :lay :roll
|
|
332
|
+
|
|
333
|
+
def overlay(
|
|
334
|
+
reel,
|
|
335
|
+
at: 0,
|
|
336
|
+
transition: nil,
|
|
337
|
+
duck: nil
|
|
338
|
+
)
|
|
339
|
+
fail Error, "No reel" unless reel
|
|
340
|
+
fail Error, "No time to roll (at: #{at})..." if at.to_f < 0
|
|
341
|
+
fail Error, "Video overlays are not implemented just yet, sorry..." if reel.channel?(:video)
|
|
342
|
+
fail Error, "Don't know how to duck video... yet" if duck != :audio
|
|
343
|
+
fail Error, "No audio to have ducked" unless reel.channel?(:audio)
|
|
344
|
+
Ffmprb.logger.warn "Overlays are over all the lays, so they'd better be after all the lays in the script." if @lays.empty?
|
|
345
|
+
Ffmprb.logger.warn "Ducking overlays are over all the lays, and over all the overlays, so they'd better be after all the lays and the overlays in the script." unless duck || @overducks.empty?
|
|
346
|
+
Ffmprb.logger.warn "Overlays are over all the lays, so they'd better be after all the lays in the script." if @lays.empty?
|
|
347
|
+
|
|
348
|
+
if duck
|
|
349
|
+
add_overduck reel, at.to_f, transition
|
|
350
|
+
else
|
|
351
|
+
add_overlay reel, at.to_f, transition
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def channel(medium)
|
|
356
|
+
@channels[medium]
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def channel?(medium)
|
|
360
|
+
!!channel(medium)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private
|
|
364
|
+
|
|
365
|
+
def add_lay(reel, after, transition)
|
|
366
|
+
@lays << OpenStruct.new(reel: reel, after: after, transition: transition && Transition.new(**transition))
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def add_overlay(reel, at, transition)
|
|
370
|
+
@overlays << OpenStruct.new(reel: reel, at: at, transition: transition && Transition.new(**transition))
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def add_overduck(reel, at, transition)
|
|
374
|
+
@overducks << OpenStruct.new(reel: reel, at: at, transition: transition && Transition.new(**transition))
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
require_relative 'output/transition'
|