Class: Ffmprb::Process::Output

Inherits:
Object
  • Object
show all
Defined in:
lib/ffmprb/process/output.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io, process, video:, audio:) ⇒ Output

Returns a new instance of Output.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ffmprb/process/output.rb', line 51

def initialize(io, process, video:, audio:)
  @io = self.class.resolve(io)
  @process = process
  @channels = {
    video: video && @io.channel?(:video) && OpenStruct.new(video),
    audio: audio && @io.channel?(:audio) && OpenStruct.new(audio)
  }
  if channel?(:video)
    channel(:video).resolution.to_s.split('x').each do |dim|
      fail Error, "Both dimensions of a resolution must be divisible by 2, sorry about that"  unless dim.to_i % 2 == 0
    end
  end
end

Instance Attribute Details

#ioObject (readonly)

Returns the value of attribute io.



48
49
50
# File 'lib/ffmprb/process/output.rb', line 48

def io
  @io
end

#processObject (readonly)

Returns the value of attribute process.



49
50
51
# File 'lib/ffmprb/process/output.rb', line 49

def process
  @process
end

Class Method Details

.audio_args(audio = nil) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/ffmprb/process/output.rb', line 24

def audio_args(audio=nil)
  audio = Process.output_audio_options.merge(audio.to_h)
  [].tap do |args|
    if (encoder = audio.delete(:encoder))  # NOTE extra encoder options possible
      args.concat "-c:a #{encoder}".split(' ')
    end
    if (sampling_freq = audio.delete(:sampling_freq))
      args.concat %W[-ar #{sampling_freq}]
    end
    Util.assert_options_empty! audio
  end
end

.resolve(io) ⇒ Object



37
38
39
40
41
42
43
44
# File 'lib/ffmprb/process/output.rb', line 37

def resolve(io)
  return io  unless
    io.is_a? String

  File.create(io).tap do |file|
    Ffmprb.logger.warn "Output file exists (#{file.path}), will probably overwrite"  if file.exist?
  end
end

.video_args(video = nil) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/ffmprb/process/output.rb', line 9

def video_args(video=nil)
  video = Process.output_video_options.merge(video.to_h)
  [].tap do |args|
    if (encoder = video.delete(:encoder))  # NOTE extra encoder options possible
      args.concat "-c:v #{encoder}".split(' ')
    end
    if (pixel_format = video.delete(:pixel_format))
      args.concat %W[-pix_fmt #{pixel_format}]
    end
    video.delete :resolution  # NOTE is handled otherwise
    video.delete :fps  # NOTE is handled otherwise
    Util.assert_options_empty! video
  end
end

Instance Method Details

#argsObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/ffmprb/process/output.rb', line 311

def args
  fail Error, "Must generate filters first."  unless @channel_lbl_ios

  [].tap do |args|
    io_channel_lbls = {}  # TODO ~~~spaghetti
    @channel_lbl_ios.each do |channel_lbl, io|
      (io_channel_lbls[io] ||= []) << channel_lbl
    end
    io_channel_lbls.each do |io, channel_lbls|
      channel_lbls.each do |channel_lbl|
        args.concat ['-map', "[#{channel_lbl}]"]
      end
      args.concat self.class.video_args(channel :video)  if channel? :video
      args.concat self.class.audio_args(channel :audio)  if channel? :audio
      args << io.path
    end
  end
end

#channel(medium) ⇒ Object



360
361
362
# File 'lib/ffmprb/process/output.rb', line 360

def channel(medium)
  @channels[medium]
end

#channel?(medium) ⇒ Boolean

Returns:

  • (Boolean)


364
365
366
# File 'lib/ffmprb/process/output.rb', line 364

def channel?(medium)
  !!channel(medium)
end

#filtersObject

TODO This method is exceptionally long at the moment. This is not too grand. However, structuring the code should be undertaken with care, as not to harm the composition clarity.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/ffmprb/process/output.rb', line 67

def filters
  fail Error, "Nothing to roll..."  unless
    @reels
  fail Error, "Supporting just full_screen for now, sorry."  unless
    @reels.all? &:full_screen?
  fail Error, "Supporting just a known output FPS"  unless
    !channel(:video) || (video_fps = channel(:video).fps)
  return @filters  if @filters

  idx = process.output_index(self)

  @filters = []

  # Concatting
  segments = []

  @reels.each_with_index do |curr_reel, i|

    lbl = nil

    if curr_reel.reel

      # NOTE mapping input to this lbl

      lbl = "o#{idx}rl#{i}"

      # NOTE Image-Padding to match the target resolution
      # TODO full screen only at the moment (see exception above)

      Ffmprb.logger.debug{"#{self} asking for filters of #{curr_reel.reel.io.inspect} video: #{channel(:video)}, audio: #{channel(:audio)}"}
      # NOTE may require changes if fps is different (and ffmpeg freezes)
      @filters.concat curr_reel.reel.filters_for(lbl, video: channel(:video), audio: channel(:audio))
    end

    trim_prev_at = curr_reel.after || (curr_reel.transition && 0)
    transition_length = curr_reel.transition ? curr_reel.transition.length : 0

    if trim_prev_at

      # NOTE make sure previous reel rolls _long_ enough AND then _just_ enough

      prev_lbl = segments.pop

      lbl_pad = "bl#{prev_lbl}#{i}"
      # NOTE generously padding the previous segment to support for all the cases
      @filters.concat(
        Filter.blank_source trim_prev_at + transition_length,
        channel(:video).resolution, video_fps, "#{lbl_pad}:v"
      )  if channel?(:video)
      @filters.concat(
        Filter.silent_source trim_prev_at + transition_length, "#{lbl_pad}:a"
      )  if channel?(:audio)

      if prev_lbl
        lbl_aux = lbl_pad
        lbl_pad = "pd#{prev_lbl}#{i}"
        @filters.concat(
          Filter.concat_v ["#{prev_lbl}:v", "#{lbl_aux}:v"], "#{lbl_pad}:v"
        )  if channel?(:video)
        @filters.concat(
          Filter.concat_a ["#{prev_lbl}:a", "#{lbl_aux}:a"], "#{lbl_pad}:a"
        )  if channel?(:audio)
      end

      if curr_reel.transition

        # NOTE Split the previous segment for transition

        if trim_prev_at > 0
          @filters.concat(
            Filter.split "#{lbl_pad}:v", ["#{lbl_pad}a:v", "#{lbl_pad}b:v"]
          )  if channel?(:video)
          @filters.concat(
            Filter.asplit "#{lbl_pad}:a", ["#{lbl_pad}a:a", "#{lbl_pad}b:a"]
          )  if channel?(:audio)
          lbl_pad, lbl_pad_ = "#{lbl_pad}a", "#{lbl_pad}b"
        else
          lbl_pad, lbl_pad_ = nil, lbl_pad
        end
      end

      if lbl_pad

        # NOTE Trim the previous segment finally

        new_prev_lbl = "tm#{prev_lbl}#{i}a"

        @filters.concat(
          Filter.trim 0, trim_prev_at, "#{lbl_pad}:v", "#{new_prev_lbl}:v"
        )  if channel?(:video)
        @filters.concat(
          Filter.atrim 0, trim_prev_at, "#{lbl_pad}:a", "#{new_prev_lbl}:a"
        )  if channel?(:audio)

        segments << new_prev_lbl
        Ffmprb.logger.debug{"Concatting segments: #{new_prev_lbl} pushed"}
      end

      if curr_reel.transition

        # NOTE snip the end of the previous segment and combine with this reel

        lbl_end1 = "o#{idx}tm#{i}b"
        lbl_reel = "o#{idx}tn#{i}"

        if !lbl  # no reel
          lbl_aux = "o#{idx}bk#{i}"
          @filters.concat(
            Filter.blank_source transition_length, channel(:video).resolution, video_fps, "#{lbl_aux}:v"
          )  if channel?(:video)
          @filters.concat(
            Filter.silent_source transition_length, "#{lbl_aux}:a"
          )  if channel?(:audio)
        end  # NOTE else hope lbl is long enough for the transition

        @filters.concat(
          Filter.trim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:v", "#{lbl_end1}:v"
        )  if channel?(:video)
        @filters.concat(
          Filter.atrim trim_prev_at, trim_prev_at + transition_length, "#{lbl_pad_}:a", "#{lbl_end1}:a"
        )  if channel?(:audio)

        # TODO the only supported transition, see #*lay
        @filters.concat(
          Filter.blend_v transition_length, channel(:video).resolution, video_fps, ["#{lbl_end1}:v", "#{lbl || lbl_aux}:v"], "#{lbl_reel}:v"
        ) if channel?(:video)
        @filters.concat(
          Filter.blend_a transition_length, ["#{lbl_end1}:a", "#{lbl || lbl_aux}:a"], "#{lbl_reel}:a"
        ) if channel?(:audio)

        lbl = lbl_reel
      end

    end

    segments << lbl  # NOTE can be nil
  end

  segments.compact!

  lbl_out = segments[0]

  if segments.size > 1
    lbl_out = "o#{idx}o"

    @filters.concat(
      Filter.concat_v segments.map{|s| "#{s}:v"}, "#{lbl_out}:v"
    )  if channel?(:video)
    @filters.concat(
      Filter.concat_a segments.map{|s| "#{s}:a"}, "#{lbl_out}:a"
    )  if channel?(:audio)
  end

  # Overlays

  # NOTE in-process overlays first

  @overlays.to_a.each_with_index do |over_reel, i|
    next  if over_reel.duck  # NOTE this is currently a single case of multi-process... process

    fail Error, "Video overlays are not implemented just yet, sorry..."  if over_reel.reel.channel?(:video)

    # Audio overlaying

    lbl_nxt = "o#{idx}o#{i}"

    lbl_over = "o#{idx}l#{i}"
    @filters.concat(  # NOTE audio only, see above
      over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
    )
    @filters.concat(
      Filter.copy "#{lbl_out}:v", "#{lbl_nxt}:v"
    )  if channel?(:video)
    @filters.concat(
      Filter.amix_to_first_same_volume ["#{lbl_out}:a", "#{lbl_over}:a"], "#{lbl_nxt}:a"
    )  if channel?(:audio)

    lbl_out = lbl_nxt
  end

  # NOTE multi-process overlays last

  @channel_lbl_ios = {}  # TODO this is a spaghetti machine
  @channel_lbl_ios["#{lbl_out}:v"] = io  if channel?(:video)
  @channel_lbl_ios["#{lbl_out}:a"] = io  if channel?(:audio)

  # TODO supporting just "full" overlays for now, see exception in #add_reel
  @overlays.to_a.each_with_index do |over_reel, i|

    # NOTE this is currently a single case of multi-process... process
    if over_reel.duck
      fail Error, "Don't know how to duck video... yet"  if over_reel.duck != :audio

      Ffmprb.logger.info "ATTENTION: ducking audio (due to the absence of a simple ffmpeg filter) does not support streaming main input. yet."

      # So ducking just audio here, ye?
      # TODO! check if we're on audio channel

      main_av_o = @channel_lbl_ios["#{lbl_out}:a"]
      fail Error, "Main output does not contain audio to duck"  unless main_av_o

      intermediate_extname = Process.intermediate_channel_extname video: main_av_o.channel?(:video), audio: main_av_o.channel?(:audio)
      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)
      @channel_lbl_ios.each do |channel_lbl, io|
        @channel_lbl_ios[channel_lbl] = main_av_inter_i  if io == main_av_o  # TODO ~~~spaghetti
      end
      process.proc_vis_edge process, main_av_o, :remove
      process.proc_vis_edge process, main_av_inter_i
      Ffmprb.logger.debug{"Re-routed the main audio output (#{main_av_inter_i.path}->...->#{main_av_o.path}) through the process of audio ducking"}

      over_a_i, over_a_o = File.threaded_buffered_fifo(Process.intermediate_channel_extname(audio: true, video: false), proc_vis: process)
      lbl_over = "o#{idx}l#{i}"
      @filters.concat(
        over_reel.reel.filters_for lbl_over, video: false, audio: channel(:audio)
      )
      @channel_lbl_ios["#{lbl_over}:a"] = over_a_i
      process.proc_vis_edge process, over_a_i
      Ffmprb.logger.debug{"Routed and buffering auxiliary output fifos (#{over_a_i.path}>#{over_a_o.path}) for overlay"}

      inter_i, inter_o = File.threaded_buffered_fifo(intermediate_extname, proc_vis: process)
      Ffmprb.logger.debug{"Allocated fifos to buffer media (#{inter_i.path}>#{inter_o.path}) while finding silence"}

      ignore_broken_pipes_was = process.ignore_broken_pipes  # TODO? maybe throw an exception instead?
      process.ignore_broken_pipes = true  # NOTE audio ducking process may break the overlay pipe

      Util::Thread.new "audio ducking" do
        process.proc_vis_edge main_av_inter_o, inter_i  # TODO mark it better
        silence = Ffmprb.find_silence(main_av_inter_o, inter_i)

        Ffmprb.logger.debug{
          silence_map = silence.map{|s| "#{s.start_at}-#{s.end_at}"}
          "Audio ducking with silence: [#{silence_map.join ', '}]"
        }

        Process.duck_audio inter_o, over_a_o, silence, main_av_o,
          process_options: {parent: process, ignore_broken_pipes: ignore_broken_pipes_was, timeout: process.timeout},
          video: channel(:video), audio: channel(:audio)
      end
    end
  end

  @filters
end

#input(io, video: true, audio: true) ⇒ Object



330
331
332
# File 'lib/ffmprb/process/output.rb', line 330

def input(io, video: true, audio: true)
  process.input io, video: video, audio: audio
end

#overlay(reel, at: 0, duck: nil) ⇒ Object



348
349
350
351
352
353
354
355
356
357
358
# File 'lib/ffmprb/process/output.rb', line 348

def overlay(
  reel,
  at: 0,
  duck: nil
)
  fail Error, "Nothing to overlay..."  unless reel
  fail Error, "Nothing to lay over yet..."  if @reels.to_a.empty?
  fail Error, "Ducking overlays should come last... for now"  if !duck && @overlays.to_a.last && @overlays.to_a.last.duck

  add_snip reel, at, duck
end

#roll(reel, onto: :full_screen, after: nil, transition: nil) ⇒ Object Also known as: lay



334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/ffmprb/process/output.rb', line 334

def roll(
  reel,
  onto: :full_screen,
  after: nil,
  transition: nil
)
  fail Error, "Nothing to roll..."  unless reel
  fail Error, "Supporting :transition with :after only at the moment, sorry."  unless
    !transition || after || @reels.to_a.empty?

  add_reel reel, after, transition, (onto == :full_screen)
end