ffmprb 0.7.0 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0f8aefe1bb66a17ba85e93faf30b2ac540badee5
4
- data.tar.gz: e3260e027699ad813154fc24c64049f42b39c114
3
+ metadata.gz: 6549b94d1f43f4c322b16e301e9cd53611bc7cae
4
+ data.tar.gz: cd492432f11c4184a702e2424453cd0b05387481
5
5
  SHA512:
6
- metadata.gz: 430d988b9a2c68dbdbbe7d75f1e20d3e9c2a92c2f0f905b8c461005d5667a522e9830b1fba2f88a339b131b285765401fdc6bfdc15cd1088295a017f6859f62c
7
- data.tar.gz: 0675dcaeabd664f44e497f72f96860437171661cae6250b223a9610105206e139c38f2b5b04a49fffd09ff0b886039d4754a47f27a31d32322418ce3e754ae4f
6
+ metadata.gz: 031c4f1a46a66cf6ce2b7c9557eab2f55a6128e490f02c3830762f31fc6d148f7ff8e9ab5fc920346aa877e4ca1fb0c0ef0c1f45c12157ba5394abb8479a2462
7
+ data.tar.gz: f1b8afe1c86c324d53e941fff18ba0fdc2b5dcd2f8af82517a851eeb642bb4c1af7310017352a7d38fe970e0f0f09fcf65ad6320aadef116e2e09724eead0bc5
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in ffmprb.gemspec
4
4
  gemspec
5
+
6
+ gem 'rspec_junit_formatter' # for circleCI https://circleci.com/docs/test-metadata#automatic-test-metadata-collection
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  guard :rspec,
2
2
  :cmd => 'bin/rspec',
3
- :run_all => {:cmd => 'bin/rspec --format documentation --profile'},
3
+ :run_all => {:cmd => 'bin/rspec --profile'},
4
4
  :all_after_pass => false,
5
5
  :all_on_start => false,
6
6
  :failed_mode => :focus do
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # ffmprb
2
2
  [![Gem Version](https://badge.fury.io/rb/ffmprb.svg)](http://badge.fury.io/rb/ffmprb)
3
+ [![Circle CI](https://circleci.com/gh/showbox-oss/ffmprb.svg?style=svg)](https://circleci.com/gh/showbox-oss/ffmprb)
3
4
  ## your audio/video montage friend, based on [ffmpeg](https://ffmpeg.org)
4
5
 
5
6
  A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends.
@@ -33,8 +34,9 @@ ffmpeg -y -i /tmp/inter1a.flv -filter_complex "silencedetect=d=2:n=-30dB" /tmp/i
33
34
  ffmpeg -y -i /tmp/inter2.flv -i /tmp/inter1b.wav -filter_complex "[0:v] copy [sp0:v]; [0:a] anull [sp0:a]; [sp0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, fps=fps=30 [rl0:v]; [sp0:a] anull [rl0:a]; [rl0:v] concat=1:v=1:a=0 [oo:v]; [rl0:a] concat=1:v=0:a=1 [oo:a]; [1:a] anull [ldol0:a]; [ldol0:a] volume='if(between(t, 9.5, 10.5), (-0.8*t + 8.500000000000002)/1.0, if(between(t, 0.5, 9.5), 0.9, if(between(t, -0.5, 0.5), (0.8*t + 0.5)/1.0, if(between(t, 0.0, -0.5), 0.1, if(between(t, 0.0, 0.0), 0.1, 0.1)))))':eval=frame [ol0:a]; [oo:v] copy [oo0:v]; [oo:a] [ol0:a] amix=2:duration=first [oo0:a]" -map "[oo0:v]" -map "[oo0:a]" cine.flv
34
35
  ```
35
36
  Umm... That's the idea.
36
- The docs, as well as any other part of this gem, are a work in progress.
37
- So you're very welcome to look around the [specs](https://github.com/showbox-oss/ffmprb/tree/master/spec) for the current functionality coverage.
37
+
38
+ The docs, as well as any other part of this gem, are totally a work in progress.
39
+ So you're very welcome to look around the [specs](https://github.com/showbox-oss/ffmprb/tree/master/spec) for the current functionality coverage, or in the gem's ["main"](https://github.com/showbox-oss/ffmprb/blob/master/lib/ffmprb.rb) for useful constants and configuration options.
38
40
 
39
41
  ## Installation
40
42
 
data/circle.yml CHANGED
@@ -1,3 +1,7 @@
1
1
  dependencies:
2
2
  pre:
3
- - sudo apt-add-repository -y ppa:oded-geek/multimedia; sudo apt-get update; sudo apt-get install ffmpeg
3
+ - gem install bundler --pre
4
+ - sudo apt-add-repository -y ppa:oded-geek/multimedia && sudo apt-get update && sudo apt-get install ffmpeg && sudo apt-get install libsox-fmt-mp3
5
+ test:
6
+ override:
7
+ - bundle exec rspec spec
data/lib/defaults.rb ADDED
@@ -0,0 +1,21 @@
1
+ module Ffmprb
2
+
3
+ Filter.silence_noise_max_db = -40
4
+
5
+ Process.duck_audio_volume_hi = 0.9
6
+ Process.duck_audio_volume_lo = 0.1
7
+ Process.duck_audio_transition_length = 1
8
+ Process.duck_audio_silent_min = 3
9
+ Process.timeout = 15
10
+
11
+ Util.ffmpeg_cmd = ['ffmpeg']
12
+ Util.ffprobe_cmd = ['ffprobe']
13
+ Util.cmd_timeout = 6
14
+
15
+ Util::ThreadedIoBuffer.blocks_max = 1024
16
+ Util::ThreadedIoBuffer.block_size = 64*1024
17
+ Util::ThreadedIoBuffer.timeout = 9
18
+
19
+ Util::Thread.timeout = 12
20
+
21
+ end
@@ -0,0 +1,48 @@
1
+ module Ffmprb
2
+
3
+ class File
4
+
5
+ def sample(
6
+ at: 0.01,
7
+ video: true,
8
+ audio: true,
9
+ &blk
10
+ )
11
+ audio = File.temp('.wav') if audio == true
12
+ video = File.temp('.png') if video == true
13
+
14
+ Ffmprb.logger.debug "Snap shooting files, video path: #{video ? video.path : 'NONE'}, audio path: #{audio ? audio.path : 'NONE'}"
15
+
16
+ fail Error, "Incorrect output extname (must be image)" unless !video || video.channel?(:video) && !video.channel?(:audio)
17
+ fail Error, "Incorrect audio extname (must be sound)" unless !audio || audio.channel?(:audio) && !audio.channel?(:video)
18
+ fail Error, "Can sample either video OR audio UNLESS a block is given" unless block_given? || !!audio != !!video
19
+
20
+ cmd = %W[-i #{path}]
21
+ cmd.concat %W[-deinterlace -an -ss #{at} -r 1 -vcodec mjpeg -f mjpeg #{video.path}] if video
22
+ cmd.concat %W[-vn -ss #{at} -t 1 #{audio.path}] if audio
23
+ Util.ffmpeg *cmd
24
+
25
+ return video || audio unless block_given?
26
+
27
+ begin
28
+ yield *[video || nil, audio || nil].compact
29
+ ensure
30
+ begin
31
+ video.remove if video
32
+ audio.remove if audio
33
+ Ffmprb.logger.debug "Removed sample files"
34
+ rescue
35
+ Ffmprb.logger.warn "Error removing sample files: #{$!.message}"
36
+ end
37
+ end
38
+ end
39
+ def sample_video(*video, at: 0.01, &blk)
40
+ sample at: at, video: (video.first || true), audio: false, &blk
41
+ end
42
+ def sample_audio(*audio, at: 0.01, &blk)
43
+ sample at: at, video: false, audio: (audio.first || true), &blk
44
+ end
45
+
46
+ end
47
+
48
+ end
data/lib/ffmprb/file.rb CHANGED
@@ -8,25 +8,25 @@ module Ffmprb
8
8
 
9
9
  class << self
10
10
 
11
- def buffered_fifo(extname='.tmp')
11
+ def threaded_buffered_fifo(extname='.tmp')
12
12
  input_fifo_file = temp_fifo(extname)
13
13
  output_fifo_file = temp_fifo(extname)
14
-
15
14
  Ffmprb.logger.debug "Opening #{input_fifo_file.path}>#{output_fifo_file.path} for buffering"
16
- buff = Util::IoBuffer.new(async_opener(input_fifo_file, 'r'), async_opener(output_fifo_file, 'w'))
17
-
18
- # NOTE the blocking cleanup thread to join for synchronisation
19
- thr = Util::Thread.new do
20
- buff.flush!
21
- Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} ended"
22
- input_fifo_file.remove
23
- output_fifo_file.remove
15
+ Util::Thread.new do
16
+ begin
17
+ Util::ThreadedIoBuffer.new async_opener(input_fifo_file, 'r'), async_opener(output_fifo_file, 'w')
18
+ Util::Thread.join_children!
19
+ Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} ended"
20
+ ensure
21
+ input_fifo_file.remove if input_fifo_file
22
+ output_fifo_file.remove if output_fifo_file
23
+ end
24
24
  end
25
25
  Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} started"
26
26
 
27
- # XXX see io_buffer's XXX yield buff if block_given?
27
+ # XXX see threaded_io_buffer's XXX yield buff if block_given?
28
28
 
29
- OpenStruct.new in: input_fifo_file, out: output_fifo_file, thr: thr
29
+ [input_fifo_file, output_fifo_file]
30
30
  end
31
31
 
32
32
  def create(path)
@@ -78,6 +78,7 @@ module Ffmprb
78
78
 
79
79
  protected
80
80
 
81
+ # NOTE must be timeout-safe
81
82
  def async_opener(file, mode)
82
83
  ->{
83
84
  Ffmprb.logger.debug "Trying to open #{file.path} for #{mode}-buffering"
@@ -93,7 +94,7 @@ module Ffmprb
93
94
  @path.close if @path && @path.respond_to?(:close) # NOTE specially for temp files
94
95
  path! # NOTE early (exception) raiser
95
96
  @mode = mode.to_sym
96
- raise Error, "Open for read, create for write, ??? for #{@mode}" unless %i[read write].include?(@mode)
97
+ fail Error, "Open for read, create for write, ??? for #{@mode}" unless %i[read write].include?(@mode)
97
98
  end
98
99
 
99
100
  def path
@@ -139,40 +140,6 @@ module Ffmprb
139
140
  end
140
141
 
141
142
 
142
- def sample( # NOTE can snap output (an image) or audio (a sound) or both
143
- at: 0.01,
144
- video: true,
145
- audio: nil
146
- )
147
- audio = File.temp('.mp3') if audio == true
148
- video = File.temp('.jpg') if video == true
149
-
150
- Ffmprb.logger.debug "Snap shooting files, video path: #{video ? video.path : 'NONE'}, audio path: #{audio ? audio.path : 'NONE'}"
151
-
152
- raise Error, "Incorrect output extname (must be .jpg)" unless !video || video.channel?(:video) && !video.channel?(:audio)
153
- raise Error, "Incorrect audio extname (must be .mp3)" unless !audio || audio.channel?(:audio) && !audio.channel?(:video)
154
- raise Error, "Can sample either video OR audio UNLESS a block is given" unless block_given? || !!audio != !!video
155
-
156
- cmd = ['-i', path]
157
- cmd += ['-deinterlace', '-an', '-ss', at, '-r', 1, '-vcodec', 'mjpeg', '-f', 'mjpeg', video.path] if video
158
- cmd += ['-vn', '-ss', at, '-t', 1, '-f', 'mp3', audio.path] if audio
159
- Util.ffmpeg *cmd
160
-
161
- return video || audio unless block_given?
162
-
163
- begin
164
- yield *[video || nil, audio || nil].compact
165
- ensure
166
- begin
167
- video.remove if video
168
- audio.remove if audio
169
- Ffmprb.logger.debug "Removed sample files"
170
- rescue
171
- Ffmprb.logger.warn "Error removing sample files: #{$!.message}"
172
- end
173
- end
174
- end
175
-
176
143
  # Manipulation
177
144
 
178
145
  def read
@@ -195,7 +162,7 @@ module Ffmprb
195
162
  @path.respond_to?(:path)? @path.path : @path
196
163
  ).tap do |path|
197
164
  # XXX ensure readabilty/writability/readiness
198
- raise Error, "'#{path}' is un#{@mode.to_s[0..3]}able" unless path && !path.empty?
165
+ fail Error, "'#{path}' is un#{@mode.to_s[0..3]}able" unless path && !path.empty?
199
166
  end
200
167
  end
201
168
 
@@ -204,12 +171,12 @@ module Ffmprb
204
171
  cmd = ['-v', 'quiet', '-i', path, '-print_format', 'json', '-show_format', '-show_streams']
205
172
  cmd << '-show_frames' if harder
206
173
  @probe = JSON.parse(Util::ffprobe *cmd).tap do |probe|
207
- raise Error, "This doesn't look like a ffprobable file" unless probe['streams']
174
+ fail Error, "This doesn't look like a ffprobable file" unless probe['streams']
208
175
  end
209
176
  end
210
177
 
211
178
  def image_extname?
212
- extname =~ /^\.(jpe?g|y4m)$/i
179
+ extname =~ /^\.(jpe?g|png|y4m)$/i
213
180
  end
214
181
 
215
182
  def sound_extname?
data/lib/ffmprb/filter.rb CHANGED
@@ -11,15 +11,26 @@ module Ffmprb
11
11
  end
12
12
 
13
13
  def afade_in(duration=1, input=nil, output=nil)
14
- inout "afade=in:d=#{duration}", input, output
14
+ inout "afade=in:d=#{duration}:curve=hsin", input, output
15
15
  end
16
16
 
17
17
  def afade_out(duration=1, input=nil, output=nil)
18
- inout "afade=out:d=#{duration}", input, output
18
+ inout "afade=out:d=#{duration}:curve=hsin", input, output
19
19
  end
20
20
 
21
- def amix_to_first(inputs, output=nil)
22
- inout "amix=#{[*inputs].length}:duration=first", inputs, output
21
+ def amix_to_first_same_volume(inputs, output=nil)
22
+ filters = []
23
+ new_inputs = inputs.map do |input|
24
+ if input == inputs.first
25
+ input
26
+ else
27
+ "apd#{input}".tap do |lbl_aux|
28
+ filters.concat inout('apad', input, lbl_aux)
29
+ end
30
+ end
31
+ end
32
+ filters +
33
+ inout("amix=#{inputs.length}:duration=shortest:dropout_transition=0, volume=#{inputs.length}", new_inputs, output)
23
34
  end
24
35
 
25
36
  def anull(input=nil, output=nil)
@@ -38,10 +49,15 @@ module Ffmprb
38
49
  inout "atrim=#{[st, en].compact.join ':'}, asetpts=PTS-STARTPTS", input, output
39
50
  end
40
51
 
41
- def black_source(duration, resolution=nil, fps=nil, output=nil)
42
- filter = "color=black:d=#{duration}"
43
- filter << ":s=#{resolution}" if resolution
44
- filter << ":r=#{fps}" if fps
52
+ def blank_source(duration, resolution, fps, output=nil)
53
+ color_source '0x000000@0', duration, resolution, fps, output
54
+ end
55
+
56
+ def color_source(color, duration, resolution, fps, output=nil)
57
+ filter = "color=#{color}"
58
+ filter << ":d=#{duration}"
59
+ filter << ":s=#{resolution}"
60
+ filter << ":r=#{fps}"
45
61
  inout filter, nil, output
46
62
  end
47
63
 
@@ -76,30 +92,30 @@ module Ffmprb
76
92
  def crop_exps(crop)
77
93
  exps = []
78
94
 
79
- if crop[:left] > 0
95
+ if crop[:left]
80
96
  exps << "x=in_w*#{crop[:left]}"
81
97
  end
82
98
 
83
- if crop[:top] > 0
99
+ if crop[:top]
84
100
  exps << "y=in_h*#{crop[:top]}"
85
101
  end
86
102
 
87
- if crop[:right] > 0 && crop[:left]
88
- raise Error, "Must specify two of {left, right, width} at most" if crop[:width]
103
+ if crop[:right] && crop[:left]
104
+ fail Error, "Must specify two of {left, right, width} at most" if crop[:width]
89
105
  crop[:width] = 1 - crop[:right] - crop[:left]
90
- elsif crop[:width] > 0
91
- if !crop[:left] && crop[:right] > 0
106
+ elsif crop[:width]
107
+ if !crop[:left] && crop[:right]
92
108
  crop[:left] = 1 - crop[:width] - crop[:right]
93
109
  exps << "x=in_w*#{crop[:left]}"
94
110
  end
95
111
  end
96
112
  exps << "w=in_w*#{crop[:width]}"
97
113
 
98
- if crop[:bottom] > 0 && crop[:top]
99
- raise Error, "Must specify two of {top, bottom, height} at most" if crop[:height]
114
+ if crop[:bottom] && crop[:top]
115
+ fail Error, "Must specify two of {top, bottom, height} at most" if crop[:height]
100
116
  crop[:height] = 1 - crop[:bottom] - crop[:top]
101
- elsif crop[:height] > 0
102
- if !crop[:top] && crop[:bottom] > 0
117
+ elsif crop[:height]
118
+ if !crop[:top] && crop[:bottom]
103
119
  crop[:top] = 1 - crop[:height] - crop[:bottom]
104
120
  exps << "y=in_h*#{crop[:top]}"
105
121
  end
@@ -123,15 +139,20 @@ module Ffmprb
123
139
  inout "pad=#{width}:#{height}:(#{width}-iw*min(#{width}/iw\\,#{height}/ih))/2:(#{height}-ih*min(#{width}/iw\\,#{height}/ih))/2", input, output
124
140
  end
125
141
 
142
+ def setsar(ratio, input=nil, output=nil)
143
+ inout "setsar=#{ratio}", input, output
144
+ end
145
+
126
146
  def scale(width, height, input=nil, output=nil)
127
147
  inout "scale=iw*min(#{width}/iw\\,#{height}/ih):ih*min(#{width}/iw\\,#{height}/ih)", input, output
128
148
  end
129
149
 
130
- def scale_pad_fps(width, height, fps, input=nil, output=nil)
150
+ def scale_pad_fps(width, height, _fps, input=nil, output=nil)
131
151
  inout [
132
152
  *scale(width, height),
133
153
  *pad(width, height),
134
- *fps(fps)
154
+ *setsar(1), # NOTE the scale & pad formulae damage SAR a little, unfortunately
155
+ *fps(_fps)
135
156
  ].join(', '), input, output
136
157
  end
137
158
 
@@ -151,7 +172,7 @@ module Ffmprb
151
172
 
152
173
  def transition_av(transition, resolution, fps, inputs, output=nil, video: true, audio: true)
153
174
  blend_duration = transition[:blend].to_f
154
- raise "Unsupported (yet) transition, sorry." unless
175
+ fail "Unsupported (yet) transition, sorry." unless
155
176
  transition.size == 1 && blend_duration > 0
156
177
 
157
178
  aux_lbl = "rn#{inputs.object_id}" # should be sufficiently random
@@ -168,7 +189,7 @@ module Ffmprb
168
189
  filters.concat [
169
190
  *afade_out(blend_duration, "#{inputs.first}:a", "#{aux_lbl}:a"),
170
191
  *afade_in(blend_duration, "#{inputs.last}:a", "#{auxx_lbl}:a"),
171
- *amix_to_first(["#{auxx_lbl}:a", "#{aux_lbl}:a"], "#{output}:a")
192
+ *amix_to_first_same_volume(["#{auxx_lbl}:a", "#{aux_lbl}:a"], "#{output}:a")
172
193
  ] if audio
173
194
  end
174
195
  end
@@ -184,7 +205,7 @@ module Ffmprb
184
205
  def volume_exp(volume)
185
206
  return volume unless volume.is_a?(Hash)
186
207
 
187
- raise Error, "volume cannot be empty" if volume.empty?
208
+ fail Error, "volume cannot be empty" if volume.empty?
188
209
 
189
210
  prev_at = 0.0
190
211
  prev_vol = volume[prev_at] || 1.0
@@ -203,11 +224,8 @@ module Ffmprb
203
224
  exp
204
225
  end
205
226
 
206
- def white_source(duration, resolution=nil, fps=nil, output=nil)
207
- filter = "color=white:d=#{duration}"
208
- filter << ":s=#{resolution}" if resolution
209
- filter << ":r=#{fps}" if fps
210
- inout filter, nil, output
227
+ def white_source(duration, resolution, fps, output=nil)
228
+ color_source '0xFFFFFF@1', duration, resolution, fps, output
211
229
  end
212
230
 
213
231
  def complex_options(*filters)
@@ -1,29 +1,40 @@
1
1
  module Ffmprb
2
2
 
3
- def self.find_silence(input_file, output_file)
4
- logger.debug "Finding silence (#{input_file.path}->#{output_file.path})"
5
- filters = Filter.silencedetect
6
- options = ['-i', input_file.path, *Filter.complex_options(filters), output_file.path]
7
- silence = []
8
- Util.ffmpeg(*options).split("\n").each do |line|
9
- next unless line =~ /^\[silencedetect\s.*\]\s*silence_(\w+):\s*(\d+\.?d*)/
10
- case $1
11
- when 'start'
12
- silence << OpenStruct.new(start_at: $2.to_f)
13
- when 'end'
14
- if silence.empty?
15
- silence << OpenStruct.new(start_at: 0.0, end_at: $2.to_f)
3
+ class << self
4
+
5
+ def find_silence(input_file, output_file)
6
+ logger.debug "Finding silence (#{input_file.path}->#{output_file.path})"
7
+ silence = []
8
+ Util.ffmpeg('-i', input_file.path, *silence_detect_options, output_file.path).
9
+ scan(SILENCE_DETECT_REGEX).each do |mark, time|
10
+ time = time.to_f
11
+
12
+ case mark
13
+ when 'start'
14
+ silence << OpenStruct.new(start_at: time)
15
+ when 'end'
16
+ if silence.empty?
17
+ silence << OpenStruct.new(start_at: 0.0, end_at: time)
18
+ else
19
+ fail Error, "ffmpeg is being stupid: silence_end with no silence_start" if silence.last.end_at
20
+ silence.last.end_at = time
21
+ end
16
22
  else
17
- raise Error, "ffmpeg is being stupid: silence_end with no silence_start" if silence.last.end_at
18
- silence.last.end_at = $2.to_f
23
+ Ffmprb.warn "Unknown silence mark: #{mark}"
19
24
  end
20
- else
21
- Ffmprb.warn "Unknown silence mark: #{$1}"
22
25
  end
26
+ logger.debug "Found silence (#{input_file.path}->#{output_file.path}): [#{silence.map{|t,v| "#{t}: #{v}"}}]"
27
+ silence
28
+ end
29
+
30
+ private
31
+
32
+ SILENCE_DETECT_REGEX = /\[silencedetect\s.*\]\s*silence_(\w+):\s*(\d+\.?d*)/
33
+
34
+ def silence_detect_options
35
+ @silence_detect_options ||= Filter.complex_options(Filter.silencedetect)
23
36
  end
24
37
 
25
- logger.debug "Found silence (#{input_file.path}->#{output_file.path}): [#{silence.map{|t,v| "#{t}: #{v}"}}]"
26
- silence
27
38
  end
28
39
 
29
40
  end
@@ -13,14 +13,19 @@ module Ffmprb
13
13
  self.crop_ratios = crop
14
14
  end
15
15
 
16
- def filters_for(lbl, process:, video: true, audio: true)
16
+ def filters_for(lbl, process:, output:, video: true, audio: true)
17
17
 
18
18
  # Cropping
19
19
 
20
20
  lbl_aux = "cp#{lbl}"
21
- @io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
21
+ lbl_tmp = "tmp#{lbl}"
22
+ @io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
22
23
  [
23
- *((video && channel?(:video))? Filter.crop(crop_ratios, "#{lbl_aux}:v", "#{lbl}:v"): nil),
24
+ *((video && channel?(:video))? [
25
+ Filter.crop(crop_ratios, "#{lbl_aux}:v", "#{lbl_tmp}:v"),
26
+ # XXX this fixup is temporary, leads to resolution loss on crop etc...
27
+ Filter.scale_pad_fps(output.target_width, output.target_height, output.target_fps, "#{lbl_tmp}:v", "#{lbl}:v")
28
+ ]: nil),
24
29
  *((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
25
30
  ]
26
31
  end
@@ -37,9 +42,9 @@ module Ffmprb
37
42
  ratios
38
43
  end.tap do |ratios| # NOTE validation
39
44
  next unless ratios
40
- raise "Allowed crop params are: #{CROP_PARAMS}" unless ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
45
+ fail "Allowed crop params are: #{CROP_PARAMS}" unless ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
41
46
  ratios.each do |key, value|
42
- raise Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless (0...1).include? value
47
+ fail Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless (0...1).include? value
43
48
  end
44
49
  end
45
50
  end
@@ -54,24 +59,40 @@ module Ffmprb
54
59
  @io = unfiltered
55
60
  @from, @to = from, (to.to_f == 0 ? nil : to)
56
61
 
57
- raise Error, "cut from: cannot be nil" if from.nil?
62
+ fail Error, "cut from: must be" unless from
63
+ fail Error, "cut from: must be less than to:" unless !to || from < to
58
64
  end
59
65
 
60
- def filters_for(lbl, process:, video: true, audio: true)
66
+ def filters_for(lbl, process:, output:, video: true, audio: true)
61
67
 
62
68
  # Trimming
63
69
 
64
70
  lbl_aux = "tm#{lbl}"
65
- @io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
66
- if from == 0 && !to
71
+ @io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
72
+ if to
73
+ lbl_blk = "bl#{lbl}"
74
+ lbl_pad = "pd#{lbl}"
75
+ [
76
+ *((video && channel?(:video))?
77
+ Filter.blank_source(to - from, output.target_resolution, output.target_fps, "#{lbl_blk}:v") +
78
+ Filter.concat_v(["#{lbl_aux}:v", "#{lbl_blk}:v"], "#{lbl_pad}:v") +
79
+ Filter.trim(from, to, "#{lbl_pad}:v", "#{lbl}:v")
80
+ : nil),
81
+ *((audio && channel?(:audio))?
82
+ Filter.silent_source(to - from, "#{lbl_blk}:a") +
83
+ Filter.concat_a(["#{lbl_aux}:a", "#{lbl_blk}:a"], "#{lbl_pad}:a") +
84
+ Filter.atrim(from, to, "#{lbl_pad}:a", "#{lbl}:a")
85
+ : nil)
86
+ ]
87
+ elsif from == 0
67
88
  [
68
89
  *((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
69
90
  *((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
70
91
  ]
71
- else
92
+ else # !to
72
93
  [
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)
94
+ *((video && channel?(:video))? Filter.trim(from, nil, "#{lbl_aux}:v", "#{lbl}:v"): nil),
95
+ *((audio && channel?(:audio))? Filter.atrim(from, nil, "#{lbl_aux}:a", "#{lbl}:a"): nil)
75
96
  ]
76
97
  end
77
98
  end
@@ -86,15 +107,15 @@ module Ffmprb
86
107
  @io = unfiltered
87
108
  @volume = volume
88
109
 
89
- raise Error, "volume cannot be nil" if volume.nil?
110
+ fail Error, "volume cannot be nil" if volume.nil?
90
111
  end
91
112
 
92
- def filters_for(lbl, process:, video: true, audio: true)
113
+ def filters_for(lbl, process:, output:, video: true, audio: true)
93
114
 
94
115
  # Modulating volume
95
116
 
96
117
  lbl_aux = "ld#{lbl}"
97
- @io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
118
+ @io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
98
119
  [
99
120
  *((video && channel?(:video))? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): nil),
100
121
  *((audio && channel?(:audio))? Filter.volume(@volume, "#{lbl_aux}:a", "#{lbl}:a"): nil)
@@ -116,13 +137,13 @@ module Ffmprb
116
137
  ['-i', @io.path]
117
138
  end
118
139
 
119
- def filters_for(lbl, process:, video: true, audio: true)
140
+ def filters_for(lbl, process:, output:, video: true, audio: true)
120
141
 
121
142
  # Channelling
122
143
 
123
144
  if @io.respond_to?(:filters_for)
124
145
  lbl_aux = "au#{lbl}"
125
- @io.filters_for(lbl_aux, process: process, video: video, audio: audio) +
146
+ @io.filters_for(lbl_aux, process: process, output: output, video: video, audio: audio) +
126
147
  [
127
148
  *((video && @io.channel?(:video))?
128
149
  (channel?(:video)? Filter.copy("#{lbl_aux}:v", "#{lbl}:v"): Filter.nullsink("#{lbl_aux}:v")):
@@ -135,7 +156,8 @@ module Ffmprb
135
156
  in_lbl = process[self]
136
157
  raise Error, "Data corruption" unless in_lbl
137
158
  [
138
- *(video && @io.channel?(:video) && channel?(:video)? Filter.copy("#{in_lbl}:v", "#{lbl}:v"): nil),
159
+ # XXX this fixup is temporary, leads to resolution loss on crop etc... *(video && @io.channel?(:video) && channel?(:video)? Filter.copy("#{in_lbl}:v", "#{lbl}:v"): nil),
160
+ *(video && @io.channel?(:video) && channel?(:video)? Filter.scale_pad_fps(output.target_width, output.target_height, output.target_fps, "#{in_lbl}:v", "#{lbl}:v"): nil),
139
161
  *(audio && @io.channel?(:audio) && channel?(:audio)? Filter.anull("#{in_lbl}:a", "#{lbl}:a"): nil)
140
162
  ]
141
163
  end
@@ -157,6 +179,10 @@ module Ffmprb
157
179
  Cut.new self, from: from, to: to
158
180
  end
159
181
 
182
+ def mute
183
+ Loud.new self, volume: 0
184
+ end
185
+
160
186
  def volume(vol)
161
187
  Loud.new self, volume: vol
162
188
  end
@@ -174,9 +200,11 @@ module Ffmprb
174
200
 
175
201
  case io
176
202
  when /^\/\w/
177
- File.open io
203
+ File.open(io).tap do |file|
204
+ Ffmprb.logger.warn "Input file does no exist (#{file.path}), will probably fail" unless file.exist?
205
+ end
178
206
  else
179
- raise Error, "Cannot resolve input: #{io}"
207
+ fail Error, "Cannot resolve input: #{io}"
180
208
  end
181
209
  end
182
210