ffmprb 0.7.5 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Guardfile +2 -2
- data/README.md +121 -15
- data/ffmprb.gemspec +2 -1
- data/lib/defaults.rb +5 -1
- data/lib/ffmprb/file.rb +2 -4
- data/lib/ffmprb/filter.rb +97 -65
- data/lib/ffmprb/process/input/chain_base.rb +29 -0
- data/lib/ffmprb/process/input/channeled.rb +40 -0
- data/lib/ffmprb/process/input/cropped.rb +70 -0
- data/lib/ffmprb/process/input/cut.rb +66 -0
- data/lib/ffmprb/process/input/looping.rb +102 -0
- data/lib/ffmprb/process/input/loud.rb +42 -0
- data/lib/ffmprb/process/input/temp.rb +26 -0
- data/lib/ffmprb/process/input.rb +39 -180
- data/lib/ffmprb/process/output.rb +140 -119
- data/lib/ffmprb/process.rb +68 -27
- data/lib/ffmprb/util/synchro.rb +1 -1
- data/lib/ffmprb/util/thread.rb +2 -2
- data/lib/ffmprb/util/threaded_io_buffer.rb +48 -36
- data/lib/ffmprb/util.rb +12 -5
- data/lib/ffmprb/version.rb +1 -1
- data/lib/ffmprb.rb +6 -6
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 161070550653011018121ce41eed46e7c762bfb5
|
4
|
+
data.tar.gz: 75ab98311631bdcf5492d6ec9cb186d81139737f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a0cf9ba9bf75eebad6401578df7161acf208f3b1a926caa8b6007f29723be21690b5fbb6a5b53f5d046d26b1978358bfe08f15f72c1a9ed39a8327dff31ad1a
|
7
|
+
data.tar.gz: 0339770c72cddda4ff50cfd98b56366573f90d110fa6c30e421d16d6bcabbf5a6d11014fbaeaca468594160bbc00da0495d984033491b26fd044ac77f00e421a
|
data/Guardfile
CHANGED
data/README.md
CHANGED
@@ -1,23 +1,19 @@
|
|
1
1
|
# ffmprb
|
2
2
|
[![Gem Version](https://badge.fury.io/rb/ffmprb.svg)](http://badge.fury.io/rb/ffmprb)
|
3
3
|
[![Circle CI](https://circleci.com/gh/showbox-oss/ffmprb.svg?style=svg)](https://circleci.com/gh/showbox-oss/ffmprb)
|
4
|
-
## your audio/video montage
|
4
|
+
## your audio/video montage pal, based on [ffmpeg](https://ffmpeg.org)
|
5
5
|
|
6
6
|
A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends.
|
7
7
|
|
8
8
|
Allows for code like
|
9
9
|
```ruby
|
10
|
-
|
11
|
-
a_track = Ffmprb::File.open('track.wav')
|
12
|
-
av_final = Ffmprb::File.create('cine.flv')
|
13
|
-
Ffmprb.process(av_raw, a_track, av_final) do |av_input1, a_input1, av_output1|
|
10
|
+
Ffmprb.process('flick.mp4', 'track.wav', 'cine.flv') do |av_input1, a_input1, av_output1|
|
14
11
|
|
15
12
|
in_main = input(av_input1)
|
16
|
-
in_sound = input(a_input1
|
17
|
-
output(av_output1, resolution: Ffmprb::HD_720p) do
|
13
|
+
in_sound = input(a_input1)
|
14
|
+
output(av_output1, video: {resolution: Ffmprb::HD_720p}) do
|
18
15
|
roll in_main.cut(from: 2, to: 5).crop(0.25), transition: {blend: 1}
|
19
|
-
roll in_main.cut(from: 6).volume(2), after: 2, transition: {blend: 1}
|
20
|
-
cut after: 10, transition: {blend: 1}
|
16
|
+
roll in_main.cut(from: 6, to: 16).volume(2), after: 2, transition: {blend: 1}
|
21
17
|
overlay in_sound.volume(0.8), duck: :audio
|
22
18
|
end
|
23
19
|
|
@@ -34,9 +30,8 @@ ffmpeg -y -i /tmp/inter1a.flv -filter_complex "silencedetect=d=2:n=-30dB" /tmp/i
|
|
34
30
|
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
|
35
31
|
```
|
36
32
|
Umm... That's the idea.
|
37
|
-
|
38
|
-
|
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.
|
33
|
+
The docs, as well as any other part of this gem, are a work in progress.
|
34
|
+
So you're very welcome to look around the [specs](https://github.com/showbox-oss/ffmprb/tree/master/spec) for the current functionality coverage.
|
40
35
|
|
41
36
|
## Installation
|
42
37
|
|
@@ -56,14 +51,125 @@ Or install it yourself as:
|
|
56
51
|
|
57
52
|
## DSL & Usage
|
58
53
|
|
59
|
-
|
54
|
+
The DSL strives to provide for the most common script cases in the most natural way:
|
55
|
+
you just describe what should be shown -- in an action sequence, like the following.
|
56
|
+
|
57
|
+
Play your _episode_ teaser snippet:
|
58
|
+
```ruby
|
59
|
+
lay episode.cut(to: 60), transition: {blend: 3}
|
60
|
+
```
|
61
|
+
Overlay anything after that with your channel _logo_:
|
62
|
+
```ruby
|
63
|
+
overlay logo.loop.cut(to: 33), after: 3, transition: {blend: 1} # both ways
|
64
|
+
```
|
65
|
+
Start with rolling some _intro_ flick:
|
66
|
+
```ruby
|
67
|
+
lay intro, transition: {blend: 1}
|
68
|
+
```
|
69
|
+
Overlay it with some special _badge_ sprite:
|
70
|
+
```ruby
|
71
|
+
overlay badge.loop, at: 1, transition: {burn: 1}
|
72
|
+
```
|
73
|
+
Show _title_:
|
74
|
+
```ruby
|
75
|
+
lay title, transition: {blend: 2}
|
76
|
+
```
|
77
|
+
Play some of your _episode_:
|
78
|
+
```ruby
|
79
|
+
lay episode.cut(from: 60, to: 540)
|
80
|
+
```
|
81
|
+
Oh well, roll some _promo_ material:
|
82
|
+
```ruby
|
83
|
+
lay promo, transition: {pixel: 2}
|
84
|
+
```
|
85
|
+
Play most of your _episode_:
|
86
|
+
```ruby
|
87
|
+
lay episode.cut(from: 540, to: 1080)
|
88
|
+
```
|
89
|
+
Roll the _credits_:
|
90
|
+
```ruby
|
91
|
+
overlay credits, at: 1075
|
92
|
+
```
|
93
|
+
Finish by playing your special _outro_:
|
94
|
+
```ruby
|
95
|
+
lay outro, transition: {blend: 1}
|
96
|
+
```
|
97
|
+
|
98
|
+
Anything that follows this order will work -- the script may be generated on the fly:
|
99
|
+
```ruby
|
100
|
+
transitions = [:blend, :burn, :zoom]
|
101
|
+
photos.shuffle.each do |photo|
|
102
|
+
lay photo.loop.cut(to: rand * 3), transition: {transitions.shuffle.first => 1}
|
103
|
+
end
|
104
|
+
```
|
105
|
+
All _inputs_ mentioned above must be supplied to `Ffmprb::process` as following
|
106
|
+
(the complete script as can be run with `ffmprb` CLI, see below):
|
107
|
+
```ruby
|
108
|
+
# script.ffmprb
|
109
|
+
|episode, logo, intro, badge, title, promo, credits, outro|
|
110
|
+
|
111
|
+
lay episode.cut(to: 60), transition: {blend: 3}
|
112
|
+
overlay logo.loop.cut(to: 33), after: 3, transition: {blend: 1}
|
113
|
+
lay intro, transition: {blend: 1}
|
114
|
+
overlay badge.loop, at: 1, transition: {burn: 1}
|
115
|
+
lay title, transition: {blend: 2}
|
116
|
+
lay episode.cut(from: 60, to: 540)
|
117
|
+
lay promo, transition: {pixel: 2}
|
118
|
+
lay episode.cut(from: 540, to: 1080)
|
119
|
+
overlay credits, at: 535
|
120
|
+
lay outro, transition: {blend: 1}
|
121
|
+
```
|
122
|
+
|
123
|
+
### Attention
|
124
|
+
|
125
|
+
- Ffmprb is a work in progress, and even more so than Ffmpeg itself;
|
126
|
+
use at your own risk and check thoroughly for production fitness in your project.
|
127
|
+
- Ffmprb uses threads internally, however, it is not thread-safe interface-wise:
|
128
|
+
you must not share its objects between different threads.
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
### General structure
|
60
133
|
|
134
|
+
Inside a `process` block, there are input definitions and output definitions;
|
135
|
+
naturally, the latter use the former:
|
136
|
+
```ruby
|
137
|
+
Ffmprb.process('flick.mp4', 'film.flv') do |av_input1, av_output1|
|
138
|
+
|
139
|
+
in_main = input(av_input1)
|
140
|
+
output(av_output1, video: {resolution: Ffmprb::HD_720p, fps: 25}) do
|
141
|
+
roll in_main.crop(0.05), transition: {blend: 1}
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
```
|
61
146
|
|
62
147
|
## Development
|
63
148
|
|
64
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
149
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
150
|
+
Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
151
|
+
|
152
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
153
|
+
To release a new version, update the version number in `version.rb`, and then run
|
154
|
+
`bundle exec rake release` to create a git tag for the version, push git commits
|
155
|
+
and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
156
|
+
|
157
|
+
### Threading policy
|
158
|
+
|
159
|
+
Generally, avoid using threads, but not at any cost.
|
160
|
+
If you have to use threads -- like they're already in use in the code -- please
|
161
|
+
follow these simple principles:
|
162
|
+
|
163
|
+
- A parent thread, when in normal operation, will join _all_ its child threads --
|
164
|
+
either via `#join` or `#value`.
|
165
|
+
- A child thread, when in normal _long-running_ operation, will check on its parent
|
166
|
+
thread periodically -- probably together with logging/quitting operation itself on timeouts
|
167
|
+
(either with a use of `Timeout.timeout` or otherwise):
|
168
|
+
if it's dead with exception (status=nil), the child should die with exception as well.
|
169
|
+
- To avoid confusion, do not allow Timeout exception (or other thread-management-related
|
170
|
+
errors) to escape threads (otherwise the joining parent must distinguish between
|
171
|
+
its own timout and that of a joined thread)
|
65
172
|
|
66
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
67
173
|
|
68
174
|
## Contributing
|
69
175
|
|
data/ffmprb.gemspec
CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
|
21
|
+
# NOTE I'm not happy with this dependency, and there's nothing crossplatform (= for windoze too) at the moment
|
22
|
+
spec.add_dependency 'mkfifo'
|
22
23
|
|
23
24
|
spec.add_development_dependency 'bundler', '>= 1.9.9'
|
24
25
|
spec.add_development_dependency 'byebug', '>= 4.0.5'
|
data/lib/defaults.rb
CHANGED
@@ -10,7 +10,11 @@ module Ffmprb
|
|
10
10
|
Process.duck_audio_volume_lo = 0.1
|
11
11
|
Process.timeout = 15
|
12
12
|
|
13
|
-
|
13
|
+
Process.output_video_resolution = CGA
|
14
|
+
Process.output_video_fps = 30
|
15
|
+
Process.output_audio_encoder = 'libmp3lame'
|
16
|
+
|
17
|
+
Util.ffmpeg_cmd = %w[ffmpeg -y]
|
14
18
|
Util.ffprobe_cmd = ['ffprobe']
|
15
19
|
Util.cmd_timeout = 6
|
16
20
|
|
data/lib/ffmprb/file.rb
CHANGED
@@ -24,7 +24,7 @@ module Ffmprb
|
|
24
24
|
end
|
25
25
|
Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} started"
|
26
26
|
|
27
|
-
#
|
27
|
+
# TODO see threaded_io_buffer's XXXs: yield buff if block_given?
|
28
28
|
|
29
29
|
[input_fifo_file, output_fifo_file]
|
30
30
|
end
|
@@ -76,8 +76,6 @@ module Ffmprb
|
|
76
76
|
::File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('', 'p' + extname)
|
77
77
|
end
|
78
78
|
|
79
|
-
protected
|
80
|
-
|
81
79
|
# NOTE must be timeout-safe
|
82
80
|
def async_opener(file, mode)
|
83
81
|
->{
|
@@ -161,7 +159,7 @@ module Ffmprb
|
|
161
159
|
( # NOTE specially for temp files
|
162
160
|
@path.respond_to?(:path)? @path.path : @path
|
163
161
|
).tap do |path|
|
164
|
-
#
|
162
|
+
# TODO ensure readabilty/writability/readiness
|
165
163
|
fail Error, "'#{path}' is un#{@mode.to_s[0..3]}able" unless path && !path.empty?
|
166
164
|
end
|
167
165
|
end
|
data/lib/ffmprb/filter.rb
CHANGED
@@ -2,6 +2,8 @@ module Ffmprb
|
|
2
2
|
|
3
3
|
module Filter
|
4
4
|
|
5
|
+
class Error < Ffmprb::Error; end
|
6
|
+
|
5
7
|
class << self
|
6
8
|
|
7
9
|
attr_accessor :silence_noise_max_db
|
@@ -10,12 +12,12 @@ module Ffmprb
|
|
10
12
|
inout "alphamerge", inputs, output
|
11
13
|
end
|
12
14
|
|
13
|
-
def afade_in(duration
|
14
|
-
inout "afade=in:d
|
15
|
+
def afade_in(duration, input=nil, output=nil)
|
16
|
+
inout "afade=in:d=%{duration}:curve=hsin", input, output, duration: duration
|
15
17
|
end
|
16
18
|
|
17
|
-
def afade_out(duration
|
18
|
-
inout "afade=out:d
|
19
|
+
def afade_out(duration, input=nil, output=nil)
|
20
|
+
inout "afade=out:d=%{duration}:curve=hsin", input, output, duration: duration
|
19
21
|
end
|
20
22
|
|
21
23
|
def amix_to_first_same_volume(inputs, output=nil)
|
@@ -25,12 +27,14 @@ module Ffmprb
|
|
25
27
|
input
|
26
28
|
else
|
27
29
|
"apd#{input}".tap do |lbl_aux|
|
28
|
-
filters
|
30
|
+
filters +=
|
31
|
+
inout("apad", input, lbl_aux) # NOTE we'll see if we really need this filter separate
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
32
35
|
filters +
|
33
|
-
inout("amix
|
36
|
+
inout("amix=%{inputs_count}:duration=shortest:dropout_transition=0, volume=%{inputs_count}",
|
37
|
+
new_inputs, output, inputs_count: (inputs.empty?? nil : inputs.size))
|
34
38
|
end
|
35
39
|
|
36
40
|
def anull(input=nil, output=nil)
|
@@ -45,8 +49,9 @@ module Ffmprb
|
|
45
49
|
inout "asplit", inputs, outputs
|
46
50
|
end
|
47
51
|
|
48
|
-
def atrim(st, en, input=nil, output=nil)
|
49
|
-
inout "atrim
|
52
|
+
def atrim(st, en=nil, input=nil, output=nil)
|
53
|
+
inout "atrim=%{start_end}, asetpts=PTS-STARTPTS", input, output,
|
54
|
+
start_end: [st, en].compact.join(':')
|
50
55
|
end
|
51
56
|
|
52
57
|
def blank_source(duration, resolution, fps, output=nil)
|
@@ -54,42 +59,48 @@ module Ffmprb
|
|
54
59
|
end
|
55
60
|
|
56
61
|
def color_source(color, duration, resolution, fps, output=nil)
|
57
|
-
|
58
|
-
|
59
|
-
filter << ":s=#{resolution}"
|
60
|
-
filter << ":r=#{fps}"
|
61
|
-
inout filter, nil, output
|
62
|
+
inout "color=%{color}:d=%{duration}:s=%{resolution}:r=%{fps}", nil, output,
|
63
|
+
color: color, duration: duration, resolution: resolution, fps: fps
|
62
64
|
end
|
63
65
|
|
64
|
-
def fade_out_alpha(duration
|
65
|
-
inout "fade=out:d
|
66
|
+
def fade_out_alpha(duration, input=nil, output=nil)
|
67
|
+
inout "fade=out:d=%{duration}:alpha=1", input, output, duration: duration
|
66
68
|
end
|
67
69
|
|
68
70
|
def fps(fps, input=nil, output=nil)
|
69
|
-
inout "fps=fps
|
71
|
+
inout "fps=fps=%{fps}", input, output, fps: fps
|
70
72
|
end
|
71
73
|
|
72
74
|
def concat_v(inputs, output=nil)
|
73
|
-
inout "concat
|
75
|
+
inout "concat=%{inputs_count}:v=1:a=0", inputs, output,
|
76
|
+
inputs_count: (inputs.empty?? nil : inputs.size)
|
74
77
|
end
|
75
78
|
|
76
79
|
def concat_a(inputs, output=nil)
|
77
|
-
inout "concat
|
80
|
+
inout "concat=%{inputs_count}:v=0:a=1", inputs, output,
|
81
|
+
inputs_count: (inputs.empty?? nil : inputs.size)
|
78
82
|
end
|
79
83
|
|
80
84
|
def concat_av(inputs, output=nil)
|
81
|
-
inout "concat
|
85
|
+
inout "concat=%{inputs_count}:v=1:a=1", inputs, output,
|
86
|
+
inputs_count: (inputs.empty? || inputs.size % 2 != 0 ? nil : inputs.size/2) # XXX meh
|
82
87
|
end
|
83
88
|
|
84
89
|
def copy(input=nil, output=nil)
|
85
90
|
inout "copy", input, output
|
86
91
|
end
|
87
92
|
|
93
|
+
# TODO unused at the moment
|
88
94
|
def crop(crop, input=nil, output=nil)
|
89
|
-
inout "crop
|
95
|
+
inout "crop=x=%{left}:y=%{top}:w=%{width}:h=%{height}", input, output, crop
|
96
|
+
end
|
97
|
+
|
98
|
+
def crop_prop(crop, input=nil, output=nil)
|
99
|
+
inout "crop=%{crop_exp}", input, output,
|
100
|
+
crop_exp: crop_prop_exps(crop).join(':')
|
90
101
|
end
|
91
102
|
|
92
|
-
def
|
103
|
+
def crop_prop_exps(crop)
|
93
104
|
exps = []
|
94
105
|
|
95
106
|
if crop[:left]
|
@@ -125,81 +136,101 @@ module Ffmprb
|
|
125
136
|
exps
|
126
137
|
end
|
127
138
|
|
128
|
-
#
|
139
|
+
# NOTE might be very useful with UGC: def cropdetect
|
129
140
|
|
130
141
|
def nullsink(input=nil)
|
131
142
|
inout "nullsink", input, nil
|
132
143
|
end
|
133
144
|
|
134
145
|
def overlay(x=0, y=0, inputs=nil, output=nil)
|
135
|
-
inout "overlay=x
|
146
|
+
inout "overlay=x=%{x}:y=%{y}:eof_action=pass", inputs, output, x: x, y: y
|
136
147
|
end
|
137
148
|
|
138
|
-
def pad(
|
139
|
-
|
149
|
+
def pad(resolution, input=nil, output=nil)
|
150
|
+
width, height = resolution.to_s.split('x')
|
151
|
+
inout [
|
152
|
+
inout("pad=%{width}:%{height}:(%{width}-iw*min(%{width}/iw\\,%{height}/ih))/2:(%{height}-ih*min(%{width}/iw\\,%{height}/ih))/2",
|
153
|
+
width: width, height: height),
|
154
|
+
*setsar(1) # NOTE the scale & pad formulae damage SAR a little, unfortunately
|
155
|
+
].join(', '), input, output
|
140
156
|
end
|
141
157
|
|
142
158
|
def setsar(ratio, input=nil, output=nil)
|
143
|
-
inout "setsar
|
159
|
+
inout "setsar=%{ratio}", input, output, ratio: ratio
|
160
|
+
end
|
161
|
+
|
162
|
+
def scale(resolution, input=nil, output=nil)
|
163
|
+
width, height = resolution.to_s.split('x')
|
164
|
+
inout [
|
165
|
+
inout("scale=iw*min(%{width}/iw\\,%{height}/ih):ih*min(%{width}/iw\\,%{height}/ih)", width: width, height: height),
|
166
|
+
*setsar(1) # NOTE the scale & pad formulae damage SAR a little, unfortunately
|
167
|
+
].join(', '), input, output
|
144
168
|
end
|
145
169
|
|
146
|
-
def
|
147
|
-
inout
|
170
|
+
def scale_pad(resolution, input=nil, output=nil)
|
171
|
+
inout [
|
172
|
+
*scale(resolution),
|
173
|
+
*pad(resolution)
|
174
|
+
].join(', '), input, output
|
148
175
|
end
|
149
176
|
|
150
|
-
def scale_pad_fps(
|
177
|
+
def scale_pad_fps(resolution, _fps, input=nil, output=nil)
|
151
178
|
inout [
|
152
|
-
*
|
153
|
-
*pad(width, height),
|
154
|
-
*setsar(1), # NOTE the scale & pad formulae damage SAR a little, unfortunately
|
179
|
+
*scale_pad(resolution),
|
155
180
|
*fps(_fps)
|
156
181
|
].join(', '), input, output
|
157
182
|
end
|
158
183
|
|
159
184
|
def silencedetect(input=nil, output=nil)
|
160
|
-
inout "silencedetect=d=1:n
|
185
|
+
inout "silencedetect=d=1:n=%{silence_noise_max_db}dB", input, output,
|
186
|
+
silence_noise_max_db: silence_noise_max_db
|
161
187
|
end
|
162
188
|
|
163
189
|
def silent_source(duration, output=nil)
|
164
|
-
inout "aevalsrc=0:d
|
190
|
+
inout "aevalsrc=0:d=%{duration}", nil, output, duration: duration
|
165
191
|
end
|
166
192
|
|
167
|
-
#
|
193
|
+
# NOTE might be very useful with transitions: def smartblur
|
168
194
|
|
169
195
|
def split(inputs=nil, outputs=nil)
|
170
196
|
inout "split", inputs, outputs
|
171
197
|
end
|
172
198
|
|
173
|
-
def
|
174
|
-
|
175
|
-
fail "Unsupported (yet) transition, sorry." unless
|
176
|
-
transition.size == 1 && blend_duration > 0
|
199
|
+
def blend_v(duration, resolution, fps, inputs, output=nil)
|
200
|
+
fail Error, "must be given 2 inputs" unless inputs.size == 2
|
177
201
|
|
178
|
-
aux_lbl = "
|
202
|
+
aux_lbl = "blnd#{inputs[0]}"
|
179
203
|
auxx_lbl = "x#{aux_lbl}"
|
180
|
-
[
|
181
|
-
|
182
|
-
|
183
|
-
*
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
204
|
+
[
|
205
|
+
*white_source(duration, resolution, fps, aux_lbl),
|
206
|
+
*inout([
|
207
|
+
*alphamerge([inputs[0], aux_lbl]),
|
208
|
+
*fade_out_alpha(duration)
|
209
|
+
].join(', '), nil, auxx_lbl),
|
210
|
+
*overlay(0, 0, [inputs[1], auxx_lbl], output),
|
211
|
+
]
|
212
|
+
end
|
213
|
+
|
214
|
+
def blend_a(duration, inputs, output=nil)
|
215
|
+
fail Error, "must be given 2 inputs" unless inputs.size == 2
|
216
|
+
|
217
|
+
aux_lbl = "blnd#{inputs[0]}"
|
218
|
+
auxx_lbl = "x#{aux_lbl}"
|
219
|
+
[
|
220
|
+
*afade_out(duration, inputs[0], aux_lbl),
|
221
|
+
*afade_in(duration, inputs[1], auxx_lbl),
|
222
|
+
*amix_to_first_same_volume([auxx_lbl, aux_lbl], output)
|
223
|
+
]
|
195
224
|
end
|
196
225
|
|
197
|
-
def trim(st, en, input=nil, output=nil)
|
198
|
-
inout "trim
|
226
|
+
def trim(st, en=nil, input=nil, output=nil)
|
227
|
+
inout "trim=%{start_end}, setpts=PTS-STARTPTS", input, output,
|
228
|
+
start_end: [st, en].compact.join(':')
|
199
229
|
end
|
200
230
|
|
201
231
|
def volume(volume, input=nil, output=nil)
|
202
|
-
inout "volume='
|
232
|
+
inout "volume='%{volume_exp}':eval=frame", input, output,
|
233
|
+
volume_exp: volume_exp(volume)
|
203
234
|
end
|
204
235
|
|
205
236
|
def volume_exp(volume)
|
@@ -234,13 +265,14 @@ module Ffmprb
|
|
234
265
|
|
235
266
|
private
|
236
267
|
|
237
|
-
def inout(filter, inputs, outputs)
|
238
|
-
|
239
|
-
filter
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
]
|
268
|
+
def inout(filter, inputs=nil, outputs=nil, **values)
|
269
|
+
values.each do |key, value|
|
270
|
+
fail Error, "#{filter} needs #{key}" if value.to_s.empty?
|
271
|
+
end
|
272
|
+
filter = filter % values
|
273
|
+
filter = "#{[*inputs].map{|s| "[#{s}]"}.join ' '} " + filter if inputs
|
274
|
+
filter = filter + " #{[*outputs].map{|s| "[#{s}]"}.join ' '}" if outputs
|
275
|
+
[filter]
|
244
276
|
end
|
245
277
|
|
246
278
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
class ChainBase < Input
|
8
|
+
|
9
|
+
def initialize(unfiltered)
|
10
|
+
@io = unfiltered
|
11
|
+
end
|
12
|
+
|
13
|
+
def unfiltered; @io; end
|
14
|
+
def unfiltered=(input); @io = input; end
|
15
|
+
|
16
|
+
|
17
|
+
def chain_copy(src_input) # XXX SPEC ME
|
18
|
+
dup.tap do |top|
|
19
|
+
top.unfiltered = unfiltered.chain_copy(src_input)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def video
|
8
|
+
Channeled.new self, audio: false
|
9
|
+
end
|
10
|
+
|
11
|
+
def audio
|
12
|
+
Channeled.new self, video: false
|
13
|
+
end
|
14
|
+
|
15
|
+
class Channeled < ChainBase
|
16
|
+
|
17
|
+
def initialize(unfiltered, video: true, audio: true)
|
18
|
+
super unfiltered
|
19
|
+
@limited_channels = {video: video, audio: audio}
|
20
|
+
end
|
21
|
+
|
22
|
+
def channel(medium)
|
23
|
+
super(medium) if @limited_channels[medium]
|
24
|
+
end
|
25
|
+
|
26
|
+
def filters_for(lbl, video:, audio:)
|
27
|
+
|
28
|
+
# Doing nothing
|
29
|
+
|
30
|
+
unfiltered.filters_for lbl,
|
31
|
+
video: channel?(:video) && video, audio: channel?(:audio) && audio
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Ffmprb
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
class Input
|
6
|
+
|
7
|
+
def crop(ratio) # NOTE ratio is either a CROP_PARAMS symbol-ratio hash or a single (global) ratio
|
8
|
+
Cropped.new self, crop: ratio
|
9
|
+
end
|
10
|
+
|
11
|
+
class Cropped < ChainBase
|
12
|
+
|
13
|
+
attr_reader :ratios
|
14
|
+
|
15
|
+
def initialize(unfiltered, crop:)
|
16
|
+
super unfiltered
|
17
|
+
self.ratios = crop
|
18
|
+
end
|
19
|
+
|
20
|
+
def filters_for(lbl, video:, audio:)
|
21
|
+
|
22
|
+
# Cropping
|
23
|
+
|
24
|
+
lbl_aux = "cp#{lbl}"
|
25
|
+
lbl_tmp = "tmp#{lbl}"
|
26
|
+
unfiltered.filters_for(lbl_aux, video: unsize(video), audio: audio) +
|
27
|
+
[
|
28
|
+
*((video && channel?(:video))? [
|
29
|
+
Filter.crop_prop(ratios, "#{lbl_aux}:v", "#{lbl_tmp}:v"),
|
30
|
+
Filter.scale_pad(video.resolution, "#{lbl_tmp}:v", "#{lbl}:v")
|
31
|
+
]: nil),
|
32
|
+
*((audio && channel?(:audio))? Filter.anull("#{lbl_aux}:a", "#{lbl}:a"): nil)
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
CROP_PARAMS = %i[top left bottom right width height]
|
39
|
+
|
40
|
+
def unsize(video)
|
41
|
+
fail Error, "requires resolution" unless video.resolution
|
42
|
+
OpenStruct.new(video).tap do |video|
|
43
|
+
video.resolution = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def ratios=(ratios)
|
48
|
+
@ratios =
|
49
|
+
if ratios.is_a?(Numeric)
|
50
|
+
{top: ratios, left: ratios, bottom: ratios, right: ratios}
|
51
|
+
else
|
52
|
+
ratios
|
53
|
+
end.tap do |ratios| # NOTE validation
|
54
|
+
fail "Allowed crop params are: #{CROP_PARAMS}" unless
|
55
|
+
ratios && ratios.respond_to?(:keys) && (ratios.keys - CROP_PARAMS).empty?
|
56
|
+
|
57
|
+
ratios.each do |key, value|
|
58
|
+
fail Error, "Crop #{key} must be between 0 and 1 (not '#{value}')" unless
|
59
|
+
(0...1).include? value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|