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 +4 -4
- data/Gemfile +2 -0
- data/Guardfile +1 -1
- data/README.md +4 -2
- data/circle.yml +5 -1
- data/lib/defaults.rb +21 -0
- data/lib/ffmprb/file/sample.rb +48 -0
- data/lib/ffmprb/file.rb +17 -50
- data/lib/ffmprb/filter.rb +46 -28
- data/lib/ffmprb/find_silence.rb +30 -19
- data/lib/ffmprb/process/input.rb +48 -20
- data/lib/ffmprb/process/output.rb +109 -111
- data/lib/ffmprb/process.rb +38 -26
- data/lib/ffmprb/util/thread.rb +87 -5
- data/lib/ffmprb/util/{io_buffer.rb → threaded_io_buffer.rb} +90 -110
- data/lib/ffmprb/util.rb +62 -31
- data/lib/ffmprb/version.rb +1 -1
- data/lib/ffmprb.rb +10 -24
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6549b94d1f43f4c322b16e301e9cd53611bc7cae
|
4
|
+
data.tar.gz: cd492432f11c4184a702e2424453cd0b05387481
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 031c4f1a46a66cf6ce2b7c9557eab2f55a6128e490f02c3830762f31fc6d148f7ff8e9ab5fc920346aa877e4ca1fb0c0ef0c1f45c12157ba5394abb8479a2462
|
7
|
+
data.tar.gz: f1b8afe1c86c324d53e941fff18ba0fdc2b5dcd2f8af82517a851eeb642bb4c1af7310017352a7d38fe970e0f0f09fcf65ad6320aadef116e2e09724eead0bc5
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# ffmprb
|
2
2
|
[](http://badge.fury.io/rb/ffmprb)
|
3
|
+
[](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
|
-
|
37
|
-
|
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
|
-
-
|
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
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
27
|
+
# XXX see threaded_io_buffer's XXX yield buff if block_given?
|
28
28
|
|
29
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
22
|
-
|
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
|
42
|
-
|
43
|
-
|
44
|
-
|
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]
|
95
|
+
if crop[:left]
|
80
96
|
exps << "x=in_w*#{crop[:left]}"
|
81
97
|
end
|
82
98
|
|
83
|
-
if crop[:top]
|
99
|
+
if crop[:top]
|
84
100
|
exps << "y=in_h*#{crop[:top]}"
|
85
101
|
end
|
86
102
|
|
87
|
-
if crop[:right]
|
88
|
-
|
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]
|
91
|
-
if !crop[:left] && crop[:right]
|
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]
|
99
|
-
|
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]
|
102
|
-
if !crop[:top] && crop[:bottom]
|
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,
|
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
|
-
*
|
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
|
-
|
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
|
-
*
|
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
|
-
|
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
|
207
|
-
|
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)
|
data/lib/ffmprb/find_silence.rb
CHANGED
@@ -1,29 +1,40 @@
|
|
1
1
|
module Ffmprb
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
data/lib/ffmprb/process/input.rb
CHANGED
@@ -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
|
-
|
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))?
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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,
|
74
|
-
*((audio && channel?(:audio))? Filter.atrim(from,
|
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
|
-
|
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
|
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
|
-
|
207
|
+
fail Error, "Cannot resolve input: #{io}"
|
180
208
|
end
|
181
209
|
end
|
182
210
|
|