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