ffmprb 0.7.0 → 0.7.3

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