ffmprb 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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