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 +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
|
[![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
|
-
|
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
|
|