ffmprb 0.7.5 → 0.9.0
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/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
|
[](http://badge.fury.io/rb/ffmprb)
|
3
3
|
[](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
|