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.



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/ffmprb/process/output.rb', line 30

def initialize(io, process, video:, audio:)
  @io = 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

#processObject (readonly)

Returns the value of attribute process.



28
29
30
# File 'lib/ffmprb/process/output.rb', line 28

def process
  @process
end

Class Method Details

.audio_cmd_options(audio = nil) ⇒ Object



19
20
21
22
23
24
# File 'lib/ffmprb/process/output.rb', line 19

def audio_cmd_options(audio=nil)
  audio = Process.output_audio_options.merge(audio.to_h || {})
  [].tap do |options|
    options.concat %W[-c:a #{audio[:encoder]}]  if audio[:encoder]
  end
end

.video_cmd_options(video = nil) ⇒ Object

XXX check for unknown options



11
12
13
14
15
16
17
# File 'lib/ffmprb/process/output.rb', line 11

def video_cmd_options(video=nil)
  video = Process.output_video_options.merge(video.to_h || {})
  [].tap do |options|
    options.concat %W[-c:v #{video[:encoder]}]  if video[:encoder]
    options.concat %W[-pix_fmt #{video[:pixel_format]}]  if video[:pixel_format]
  end
end

Instance Method Details

#channel(medium) ⇒ Object



325
326
327
# File 'lib/ffmprb/process/output.rb', line 325

def channel(medium)
  @channels[medium]
end

#channel?(medium) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#filtersObject

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



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
# File 'lib/ffmprb/process/output.rb', line 46

def filters
  fail Error, "Nothing to roll..."  unless @reels
  fail Error, "Supporting just full_screen for now, sorry."  unless @reels.all?(&:full_screen?)
  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)

      @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, channel(: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, channel(: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, channel(: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 = "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)

  # 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 = {}  # XXX 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

      # So ducking just audio here, ye?
      # XXX 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
      # XXX#181845 must really seperate channels for streaming (e.g. mp4 wouldn't stream through the fifo)
      # NOTE what really must be done here (optimisation & compatibility):
      # - output v&a through non-compressed pipes
      # - v-output will be input to the new v+a merging+encoding process
      # - a-output will go through the ducking process below and its output will be input to the m+e process above
      # - v-output will have to use another thread-buffered pipe
      main_av_inter_o = File.temp_fifo(main_av_o.extname)
      @channel_lbl_ios.each do |channel_lbl, io|
        @channel_lbl_ios[channel_lbl] = main_av_inter_o  if io == main_av_o  # XXX ~~~spaghetti
      end
      Ffmprb.logger.debug "Re-routed the main audio output (#{main_av_inter_o.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)
      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
      Ffmprb.logger.debug "Routed and buffering an auxiliary output fifos (#{over_a_i.path}>#{over_a_o.path}) for overlay"

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

      ignore_broken_pipe_was = process.ignore_broken_pipe
      process.ignore_broken_pipe = true  # NOTE audio ducking process may break the overlay pipe

      Util::Thread.new "audio ducking" do
        silence = Ffmprb.find_silence(main_av_inter_o, inter_i)

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

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

  end

  @filters
end

#optionsObject



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/ffmprb/process/output.rb', line 278

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

  options = []

  io_channel_lbls = {}  # XXX ~~~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|
      options << '-map' << "[#{channel_lbl}]"
    end
    options.concat self.class.video_cmd_options(channel :video)  if channel? :video
    options.concat self.class.audio_cmd_options(channel :audio)  if channel? :audio
    options << io.path
  end

  options
end

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



313
314
315
316
317
318
319
320
321
322
323
# File 'lib/ffmprb/process/output.rb', line 313

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



299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/ffmprb/process/output.rb', line 299

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