ffmprb 0.6.6
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 +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
|