ffmprb 0.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +12 -0
- data/README.md +72 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/guard +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/ffmprb.gemspec +30 -0
- data/lib/ffmprb/file.rb +184 -0
- data/lib/ffmprb/filter.rb +234 -0
- data/lib/ffmprb/process/input.rb +178 -0
- data/lib/ffmprb/process/output.rb +332 -0
- data/lib/ffmprb/process.rb +98 -0
- data/lib/ffmprb/util/io_buffer.rb +211 -0
- data/lib/ffmprb/util/synchro.rb +47 -0
- data/lib/ffmprb/util/thread.rb +28 -0
- data/lib/ffmprb/util.rb +89 -0
- data/lib/ffmprb/version.rb +3 -0
- data/lib/ffmprb.rb +83 -0
- metadata +181 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
class Cropped < Input
|
8
|
+
|
9
|
+
attr_reader :crop_ratios
|
10
|
+
|
11
|
+
def initialize(unfiltered, crop:)
|
12
|
+
@io = unfiltered
|
13
|
+
self.crop_ratios = crop
|
14
|
+
end
|
15
|
+
|
16
|
+
def filters_for(lbl, process:, video: true, audio: true)
|
17
|
+
|
18
|
+
# Cropping
|
19
|
+
|
20
|
+
lbl_aux = "cp#{lbl}"
|
21
|
+
@io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
|
22
|
+
[
|
23
|
+
*((video && channel?(:video))? Filter.crop(crop_ratios, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
24
|
+
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
CROP_PARAMS = %i[top left bottom right width height]
|
31
|
+
|
32
|
+
def crop_ratios=(ratios)
|
33
|
+
@crop_ratios =
|
34
|
+
if ratios.is_a?(Numeric)
|
35
|
+
{top: ratios, left: ratios, bottom: ratios, right: ratios}
|
36
|
+
else
|
37
|
+
ratios
|
38
|
+
end.tap do |ratios| # NOTE validation
|
39
|
+
next unless ratios
|
40
|
+
raise "Allowed crop params are: #{CROP_PARAMS}" unless ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
|
41
|
+
ratios.each do |key, value|
|
42
|
+
raise Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless (0...1).include? value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class Cut < Input
|
50
|
+
|
51
|
+
attr_reader :from, :to
|
52
|
+
|
53
|
+
def initialize(unfiltered, from:, to:)
|
54
|
+
@io = unfiltered
|
55
|
+
@from, @to = from, (to.to_f == 0 ? nil : to)
|
56
|
+
|
57
|
+
raise Error, "cut from: cannot be nil" if from.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def filters_for(lbl, process:, video: true, audio: true)
|
61
|
+
|
62
|
+
# Trimming
|
63
|
+
|
64
|
+
lbl_aux = "tm#{lbl}"
|
65
|
+
@io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
|
66
|
+
if from == 0 && !to
|
67
|
+
[
|
68
|
+
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
69
|
+
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
70
|
+
]
|
71
|
+
else
|
72
|
+
[
|
73
|
+
*((video && channel?(:video))? Filter.trim(from, to, "#{lbl_aux}:v", "#{lbl}:v"): nil),
|
74
|
+
*((audio && channel?(:audio))? Filter.atrim(from, to, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
75
|
+
]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
class Loud < Input
|
82
|
+
|
83
|
+
attr_reader :from, :to
|
84
|
+
|
85
|
+
def initialize(unfiltered, volume:)
|
86
|
+
@io = unfiltered
|
87
|
+
@volume = volume
|
88
|
+
|
89
|
+
raise Error, "volume cannot be nil" if volume.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
def filters_for(lbl, process:, video: true, audio: true)
|
93
|
+
|
94
|
+
# Modulating volume
|
95
|
+
|
96
|
+
lbl_aux = "ld#{lbl}"
|
97
|
+
@io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
|
98
|
+
[
|
99
|
+
*((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
|
100
|
+
*((audio && channel?(:audio))? Filter.volume(@volume, "#{lbl_aux}:a", "#{lbl}:a"): nil)
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def initialize(io, only: nil)
|
108
|
+
@io = io
|
109
|
+
@channels = [*only]
|
110
|
+
@channels = nil if @channels.empty?
|
111
|
+
raise Error, "Inadequate A/V channels" if
|
112
|
+
@io.respond_to?(:channel?) &&
|
113
|
+
[:video, :audio].any?{|medium| !@io.channel?(medium) && channel?(medium, true)}
|
114
|
+
end
|
115
|
+
|
116
|
+
def options
|
117
|
+
" -i #{@io.path}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def filters_for(lbl, process:, video: true, audio: true)
|
121
|
+
|
122
|
+
# Channelling
|
123
|
+
|
124
|
+
if @io.respond_to?(:filters_for) # NOTE assuming @io.respond_to?(:channel?)
|
125
|
+
lbl_aux = "au#{lbl}"
|
126
|
+
@io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
|
127
|
+
[
|
128
|
+
*((video && @io.channel?(:video))?
|
129
|
+
(channel?(:video)? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): Filter.nullsink("#{lbl_aux}:v")):
|
130
|
+
nil),
|
131
|
+
*((audio && @io.channel?(:audio))?
|
132
|
+
(channel?(:audio)? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): Filter.anullsink("#{lbl_aux}:a")):
|
133
|
+
nil)
|
134
|
+
]
|
135
|
+
else
|
136
|
+
in_lbl = process[self]
|
137
|
+
raise Error, "Data corruption" unless in_lbl
|
138
|
+
[
|
139
|
+
*(video && channel?(:video)? Filter.copy("#{in_lbl}:v", "#{lbl}:v"): nil),
|
140
|
+
*(audio && channel?(:audio)? Filter.anull("#{in_lbl}:a", "#{lbl}:a"): nil)
|
141
|
+
]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def video
|
146
|
+
Input.new self, only: :video
|
147
|
+
end
|
148
|
+
|
149
|
+
def audio
|
150
|
+
Input.new self, only: :audio
|
151
|
+
end
|
152
|
+
|
153
|
+
def crop(ratio) # NOTE ratio is either a CROP_PARAMS symbol-ratio hash or a single (global) ratio
|
154
|
+
Cropped.new self, crop: ratio
|
155
|
+
end
|
156
|
+
|
157
|
+
def cut(from: 0, to: nil)
|
158
|
+
Cut.new self, from: from, to: to
|
159
|
+
end
|
160
|
+
|
161
|
+
def volume(vol)
|
162
|
+
Loud.new self, volume: vol
|
163
|
+
end
|
164
|
+
|
165
|
+
# XXX? protected
|
166
|
+
|
167
|
+
def channel?(medium, force=false)
|
168
|
+
return @channels && @channels.include?(medium) if force
|
169
|
+
|
170
|
+
(!@channels || @channels.include?(medium)) &&
|
171
|
+
(!@io.respond_to?(:channel?) || @io.channel?(medium))
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Output
|
6
|
+
|
7
|
+
def initialize(io, only: nil, resolution: Ffmprb::QVGA, fps: 30)
|
8
|
+
@io = io
|
9
|
+
@channels = [*only]
|
10
|
+
@channels = nil if @channels.empty?
|
11
|
+
@resolution = resolution
|
12
|
+
@fps = 30
|
13
|
+
end
|
14
|
+
|
15
|
+
def options(process)
|
16
|
+
# XXX TODO manage stream labels through process
|
17
|
+
raise Error, "Nothing to roll..." if @reels.select(&:reel).empty?
|
18
|
+
raise Error, "Supporting just full_screen for now, sorry." unless @reels.all?(&:full_screen?)
|
19
|
+
|
20
|
+
filters = []
|
21
|
+
|
22
|
+
# Concatting
|
23
|
+
segments = []
|
24
|
+
Ffmprb.logger.debug "Concatting segments: start"
|
25
|
+
|
26
|
+
@reels.each_with_index do |curr_reel, i|
|
27
|
+
|
28
|
+
lbl = nil
|
29
|
+
|
30
|
+
if curr_reel.reel
|
31
|
+
|
32
|
+
# NOTE mapping input to this lbl
|
33
|
+
|
34
|
+
lbl = "rl#{i}"
|
35
|
+
lbl_aux = "sp#{i}"
|
36
|
+
|
37
|
+
# NOTE Image-Scaling & Image-Padding to match the target resolution
|
38
|
+
# XXX full screen only (see exception above)
|
39
|
+
|
40
|
+
filters +=
|
41
|
+
curr_reel.reel.filters_for(lbl_aux, process: process,
|
42
|
+
video: channel?(:video), audio: channel?(:audio))
|
43
|
+
filters +=
|
44
|
+
Filter.scale_pad_fps(target_width, target_height, target_fps, "#{lbl_aux}:v", "#{lbl}:v") if
|
45
|
+
channel?(:video)
|
46
|
+
filters +=
|
47
|
+
Filter.anull("#{lbl_aux}:a", "#{lbl}:a") if
|
48
|
+
channel?(:audio)
|
49
|
+
end
|
50
|
+
|
51
|
+
trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
|
52
|
+
|
53
|
+
if trim_prev_at
|
54
|
+
|
55
|
+
# NOTE make sure previous reel rolls _long_ enough AND then _just_ enough
|
56
|
+
|
57
|
+
prev_lbl = segments.pop
|
58
|
+
Ffmprb.logger.debug "Concatting segments: #{prev_lbl} popped"
|
59
|
+
|
60
|
+
lbl_pad = "bl#{prev_lbl}#{i}"
|
61
|
+
# 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)
|
68
|
+
|
69
|
+
if prev_lbl
|
70
|
+
lbl_aux = lbl_pad
|
71
|
+
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)
|
78
|
+
end
|
79
|
+
|
80
|
+
if curr_reel.transition
|
81
|
+
|
82
|
+
# NOTE Split the previous segment for transition
|
83
|
+
|
84
|
+
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)
|
91
|
+
lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
|
92
|
+
else
|
93
|
+
lbl_pad, lbl_pad_ = nil, lbl_pad
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if lbl_pad
|
98
|
+
|
99
|
+
# NOTE Trim the previous segment finally
|
100
|
+
|
101
|
+
new_prev_lbl = "tm#{prev_lbl}#{i}a"
|
102
|
+
|
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)
|
109
|
+
|
110
|
+
segments << new_prev_lbl
|
111
|
+
Ffmprb.logger.debug "Concatting segments: #{new_prev_lbl} pushed"
|
112
|
+
end
|
113
|
+
|
114
|
+
if curr_reel.transition
|
115
|
+
|
116
|
+
# NOTE snip the end of the previous segment and combine with this reel
|
117
|
+
|
118
|
+
lbl_end1 = "tm#{i}b"
|
119
|
+
lbl_reel = "tn#{i}"
|
120
|
+
if !lbl # no reel
|
121
|
+
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)
|
128
|
+
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))
|
138
|
+
lbl = lbl_reel
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
segments << lbl # NOTE can be nil
|
144
|
+
Ffmprb.logger.debug "Concatting segments: #{lbl} pushed"
|
145
|
+
end
|
146
|
+
|
147
|
+
segments.compact!
|
148
|
+
|
149
|
+
lbl_out = 'oo'
|
150
|
+
|
151
|
+
filters +=
|
152
|
+
Filter.concat_v(segments.map{|s| "#{s}:v"}, "#{lbl_out}:v") if channel?(:video)
|
153
|
+
filters +=
|
154
|
+
Filter.concat_a(segments.map{|s| "#{s}:a"}, "#{lbl_out}:a") if channel?(:audio)
|
155
|
+
|
156
|
+
# Overlays
|
157
|
+
|
158
|
+
# NOTE in-process overlays first
|
159
|
+
|
160
|
+
@overlays.to_a.each_with_index do |over_reel, i|
|
161
|
+
|
162
|
+
# XXX this is currently a single case of multi-process... process
|
163
|
+
unless over_reel.duck
|
164
|
+
raise Error, "Video overlays are not implemented just yet, sorry..." if over_reel.reel.channel?(:video)
|
165
|
+
|
166
|
+
# Audio overlaying
|
167
|
+
|
168
|
+
lbl_nxt = "oo#{i}"
|
169
|
+
|
170
|
+
lbl_over = "ol#{i}"
|
171
|
+
filters +=
|
172
|
+
over_reel.reel.filters_for(lbl_over, process: process) # NOTE audio only, see above
|
173
|
+
|
174
|
+
filters +=
|
175
|
+
Filter.copy("#{lbl_out}:v", "#{lbl_nxt}:v") if channel?(:video)
|
176
|
+
filters +=
|
177
|
+
Filter.amix(["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a") if channel?(:audio)
|
178
|
+
|
179
|
+
lbl_out = lbl_nxt
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
# NOTE multi-process overlays last
|
185
|
+
|
186
|
+
channel_lbl_ios = {} # XXX this is a spaghetti machine
|
187
|
+
channel_lbl_ios["#{lbl_out}:v"] = @io if channel?(:video)
|
188
|
+
channel_lbl_ios["#{lbl_out}:a"] = @io if channel?(:audio)
|
189
|
+
|
190
|
+
# XXX supporting just "full" overlays for now, see exception in #add_reel
|
191
|
+
@overlays.to_a.each_with_index do |over_reel, i|
|
192
|
+
|
193
|
+
# XXX this is currently a single case of multi-process... process
|
194
|
+
if over_reel.duck
|
195
|
+
raise Error, "Don't know how to duck video... yet" if over_reel.duck != :audio
|
196
|
+
|
197
|
+
# So ducking just audio here, ye?
|
198
|
+
|
199
|
+
main_a_o = channel_lbl_ios["#{lbl_out}:a"]
|
200
|
+
raise Error, "Main output does not contain audio to duck" unless main_a_o
|
201
|
+
# XXX#181845 must really seperate channels for streaming (e.g. mp4 wouldn't stream through the fifo)
|
202
|
+
main_a_inter_o = File.temp_fifo(main_a_o.extname)
|
203
|
+
channel_lbl_ios.each do |channel_lbl, io|
|
204
|
+
channel_lbl_ios[channel_lbl] = main_a_inter_o if io == main_a_o # XXX ~~~spaghetti
|
205
|
+
end
|
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"
|
207
|
+
|
208
|
+
overlay_io = File.buffered_fifo(Process.intermediate_channel_extname :audio)
|
209
|
+
process.threaded overlay_io.thr
|
210
|
+
lbl_over = "ol#{i}"
|
211
|
+
filters +=
|
212
|
+
over_reel.reel.filters_for(lbl_over, process: process, video: false, audio: true)
|
213
|
+
channel_lbl_ios["#{lbl_over}:a"] = overlay_io.in
|
214
|
+
Ffmprb.logger.debug "Routed and buffering an auxiliary output fifos (#{overlay_io.in.path}>#{overlay_io.out.path}) for overlay"
|
215
|
+
|
216
|
+
inter_io = File.buffered_fifo(main_a_inter_o.extname)
|
217
|
+
process.threaded inter_io.thr
|
218
|
+
Ffmprb.logger.debug "Allocated fifos to buffer media (#{inter_io.in.path}>#{inter_io.out.path}) while finding silence"
|
219
|
+
|
220
|
+
thr = Util::Thread.new "audio ducking" do
|
221
|
+
silence = Ffmprb.find_silence(main_a_inter_o, inter_io.in)
|
222
|
+
|
223
|
+
Ffmprb.logger.debug "Audio ducking with silence: [#{silence.map{|s| "#{s.start_at}-#{s.end_at}"}.join ', '}]"
|
224
|
+
|
225
|
+
Process.duck_audio inter_io.out, overlay_io.out, silence, main_a_o,
|
226
|
+
video: (channel?(:video)? {resolution: target_resolution, fps: target_fps}: false)
|
227
|
+
end
|
228
|
+
process.threaded thr
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
Filter.complex_options(filters).tap do |options|
|
234
|
+
|
235
|
+
io_channel_lbls = {} # XXX ~~~spaghetti
|
236
|
+
channel_lbl_ios.each do |channel_lbl, io|
|
237
|
+
(io_channel_lbls[io] ||= []) << channel_lbl
|
238
|
+
end
|
239
|
+
io_channel_lbls.each do |io, channel_lbls|
|
240
|
+
channel_lbls.each do |channel_lbl|
|
241
|
+
options << " -map \"[#{channel_lbl}]\""
|
242
|
+
end
|
243
|
+
options << " #{io.path}"
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def cut(
|
250
|
+
after: nil,
|
251
|
+
transition: nil
|
252
|
+
)
|
253
|
+
raise Error, "Nothing to cut yet..." if @reels.empty? || @reels.last.reel.nil?
|
254
|
+
|
255
|
+
add_reel nil, after, transition, @reels.last.full_screen?
|
256
|
+
end
|
257
|
+
|
258
|
+
def overlay(
|
259
|
+
reel,
|
260
|
+
at: 0,
|
261
|
+
duck: nil
|
262
|
+
)
|
263
|
+
raise Error, "Nothing to overlay..." unless reel
|
264
|
+
raise Error, "Nothing to lay over yet..." if @reels.to_a.empty?
|
265
|
+
raise Error, "Ducking overlays should come last... for now" if !duck && @overlays.to_a.last && @overlays.to_a.last.duck
|
266
|
+
|
267
|
+
(@overlays ||= []) <<
|
268
|
+
OpenStruct.new(reel: reel, at: at, duck: duck)
|
269
|
+
end
|
270
|
+
|
271
|
+
def roll(
|
272
|
+
reel,
|
273
|
+
onto: :full_screen,
|
274
|
+
after: nil,
|
275
|
+
transition: nil
|
276
|
+
)
|
277
|
+
raise Error, "Nothing to roll..." unless reel
|
278
|
+
raise Error, "Supporting :transition with :after only at the moment, sorry." unless
|
279
|
+
!transition || after || @reels.to_a.empty?
|
280
|
+
|
281
|
+
add_reel reel, after, transition, (onto == :full_screen)
|
282
|
+
end
|
283
|
+
|
284
|
+
# XXX? protected
|
285
|
+
|
286
|
+
def channel?(medium, force=false)
|
287
|
+
return @channels && @channels.include?(medium) if force
|
288
|
+
|
289
|
+
(!@channels || @channels.include?(medium)) &&
|
290
|
+
reels_channel?(medium)
|
291
|
+
end
|
292
|
+
|
293
|
+
private
|
294
|
+
|
295
|
+
def reels_channel?(medium)
|
296
|
+
@reels.to_a.all?{|r| !r.reel || r.reel.channel?(medium)}
|
297
|
+
end
|
298
|
+
|
299
|
+
def add_reel(reel, after, transition, full_screen)
|
300
|
+
raise Error, "No time to roll..." if after && after.to_f <= 0
|
301
|
+
raise Error, "Partial (not coming last in process) overlays are currently unsupported, sorry." unless @overlays.to_a.empty?
|
302
|
+
|
303
|
+
# NOTE limited functionality (see exception in Filter.transition_av): transition = {effect => duration}
|
304
|
+
transition_length = transition.to_h.max_by{|k,v| v}.to_a.last.to_f
|
305
|
+
|
306
|
+
(@reels ||= []) <<
|
307
|
+
OpenStruct.new(reel: reel, after: after, transition: transition, transition_length: transition_length, full_screen?: full_screen)
|
308
|
+
end
|
309
|
+
|
310
|
+
def target_width
|
311
|
+
@target_width ||= @resolution.to_s.split('x')[0].to_i.tap do |width|
|
312
|
+
raise Error, "Width (#{width}) must be divisible by 2, sorry" unless width % 2 == 0
|
313
|
+
end
|
314
|
+
end
|
315
|
+
def target_height
|
316
|
+
@target_height ||= @resolution.to_s.split('x')[1].to_i.tap do |height|
|
317
|
+
raise Error, "Height (#{height}) must be divisible by 2, sorry" unless height % 2 == 0
|
318
|
+
end
|
319
|
+
end
|
320
|
+
def target_resolution
|
321
|
+
"#{target_width}x#{target_height}"
|
322
|
+
end
|
323
|
+
|
324
|
+
def target_fps
|
325
|
+
@fps
|
326
|
+
end
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
|
332
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'ffmprb/process/input'
|
2
|
+
require 'ffmprb/process/output'
|
3
|
+
|
4
|
+
module Ffmprb
|
5
|
+
|
6
|
+
class Process
|
7
|
+
|
8
|
+
def self.intermediate_channel_extname(*media)
|
9
|
+
if media == [:video]
|
10
|
+
'.y4m'
|
11
|
+
elsif media == [:audio]
|
12
|
+
'.wav'
|
13
|
+
elsif media.sort == [:audio, :video]
|
14
|
+
'.flv'
|
15
|
+
else
|
16
|
+
raise Error, "I don't know how to channel [#{media.join ', '}]"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.duck_audio(av_main_i, a_overlay_i, silence, av_main_o, video: {resolution: Ffmprb::QVGA, fps: 30})
|
21
|
+
Ffmprb.process(av_main_i, a_overlay_i, silence, av_main_o) do |main_input, overlay_input, duck_data, main_output|
|
22
|
+
|
23
|
+
in_main = input(main_input, **(video ? {} : {only: :audio}))
|
24
|
+
in_over = input(overlay_input, only: :audio)
|
25
|
+
prev_silent_at = 0
|
26
|
+
output(main_output, **(video ? {resolution: video[:resolution], fps: video[:fps]} : {})) do
|
27
|
+
roll in_main
|
28
|
+
ducked_overlay_volume = {0.0 => 0.1}
|
29
|
+
duck_data.each do |silent|
|
30
|
+
next if silent.end_at && silent.start_at && (silent.end_at - silent.start_at) < 3
|
31
|
+
ducked_overlay_volume.merge!(
|
32
|
+
(silent.start_at - 0.5) => 0.1,
|
33
|
+
(silent.start_at + 0.5) => 0.9
|
34
|
+
) if silent.start_at
|
35
|
+
ducked_overlay_volume.merge!(
|
36
|
+
(silent.end_at - 0.5) => 0.9,
|
37
|
+
(silent.end_at + 0.5) => 0.1
|
38
|
+
) if silent.end_at
|
39
|
+
end
|
40
|
+
overlay in_over.volume ducked_overlay_volume
|
41
|
+
Ffmprb.logger.debug "Ducking audio with volumes: {#{ducked_overlay_volume.map{|t,v| "#{t}: #{v}"}.join ', '}}"
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(*args, &blk)
|
48
|
+
@inputs = []
|
49
|
+
end
|
50
|
+
|
51
|
+
def input(io, only: nil)
|
52
|
+
Input.new(io, only: only).tap do |inp|
|
53
|
+
@inputs << inp
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def output(io, only: nil, resolution: Ffmprb::QVGA, fps: 30, &blk)
|
58
|
+
raise Error, "Just one output for now, sorry." if @output
|
59
|
+
|
60
|
+
@output = Output.new(io, only: only, resolution: resolution).tap do |out|
|
61
|
+
out.instance_exec &blk
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def run
|
66
|
+
Util.ffmpeg command
|
67
|
+
@threaded.to_a.each &:join
|
68
|
+
end
|
69
|
+
|
70
|
+
def [](obj)
|
71
|
+
case obj
|
72
|
+
when Input
|
73
|
+
@inputs.find_index(obj)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO deserves a better solution
|
78
|
+
def threaded(thr)
|
79
|
+
(@threaded ||= []) << thr
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def command
|
85
|
+
input_options + output_options
|
86
|
+
end
|
87
|
+
|
88
|
+
def input_options
|
89
|
+
@inputs.map(&:options).join
|
90
|
+
end
|
91
|
+
|
92
|
+
def output_options
|
93
|
+
@output.options self
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|