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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 510aa0451bf42ca9dc3dad8268bc017cce41f331
4
- data.tar.gz: 9abf39c9c5081038aecf607cf4c16fdc49788514
3
+ metadata.gz: 161070550653011018121ce41eed46e7c762bfb5
4
+ data.tar.gz: 75ab98311631bdcf5492d6ec9cb186d81139737f
5
5
  SHA512:
6
- metadata.gz: 364cbc89864751a3837cb6c2239a823bf964f948b3504b741754116744cd5e7e7981523e0d88d88e66b486d3c011dac9f572c6619e97985363887fe693c5e827
7
- data.tar.gz: 25e2170a21c60ced85fc43fa6c33007782b981741c12377c2aedd1f7e67e626a8ebffa40b9b7eb92272b949f4f1872295dd0ce1887ef89fdcfd892fc613dd093
6
+ metadata.gz: 6a0cf9ba9bf75eebad6401578df7161acf208f3b1a926caa8b6007f29723be21690b5fbb6a5b53f5d046d26b1978358bfe08f15f72c1a9ed39a8327dff31ad1a
7
+ data.tar.gz: 0339770c72cddda4ff50cfd98b56366573f90d110fa6c30e421d16d6bcabbf5a6d11014fbaeaca468594160bbc00da0495d984033491b26fd044ac77f00e421a
data/Guardfile CHANGED
@@ -1,8 +1,8 @@
1
1
  guard :rspec,
2
2
  :cmd => 'bin/rspec',
3
3
  :run_all => {:cmd => 'bin/rspec --profile'},
4
- :all_after_pass => false,
5
- :all_on_start => false,
4
+ :all_after_pass => true,
5
+ :all_on_start => true,
6
6
  :failed_mode => :focus do
7
7
 
8
8
  watch(%r{^spec/.+_spec\.rb$})
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 friend, based on [ffmpeg](https://ffmpeg.org)
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
- av_raw = Ffmprb::File.open('flick.mp4')
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, only: :audio)
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
- 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.
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
- TODO: Write usage instructions here
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. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
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
- spec.add_dependency 'mkfifo' # XXX I'm not happy with this dependency, and there's nothing crossplatform (= for windoze too) at the moment
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
- Util.ffmpeg_cmd = ['ffmpeg']
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
- # XXX see threaded_io_buffer's XXX yield buff if block_given?
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
- # XXX ensure readabilty/writability/readiness
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=1, input=nil, output=nil)
14
- inout "afade=in:d=#{duration}:curve=hsin", input, output
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=1, input=nil, output=nil)
18
- inout "afade=out:d=#{duration}:curve=hsin", input, output
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.concat inout('apad', input, lbl_aux)
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=#{inputs.length}:duration=shortest:dropout_transition=0, volume=#{inputs.length}", new_inputs, output)
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=#{[st, en].compact.join ':'}, asetpts=PTS-STARTPTS", input, output
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
- filter = "color=#{color}"
58
- filter << ":d=#{duration}"
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=1, input=nil, output=nil)
65
- inout "fade=out:d=#{duration}:alpha=1", input, output
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=#{fps}", input, output
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=#{[*inputs].length}:v=1:a=0", inputs, output
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=#{[*inputs].length}:v=0:a=1", inputs, output
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=#{inputs.length/2}:v=1:a=1", inputs, output
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=#{crop_exps(crop).join ':'}", input, output
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 crop_exps(crop)
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
- # XXX might be very useful with UGC: def cropdetect
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=#{x}:y=#{y}:eof_action=pass", inputs, output
146
+ inout "overlay=x=%{x}:y=%{y}:eof_action=pass", inputs, output, x: x, y: y
136
147
  end
137
148
 
138
- def pad(width, height, input=nil, output=nil)
139
- inout "pad=#{width}:#{height}:(#{width}-iw*min(#{width}/iw\\,#{height}/ih))/2:(#{height}-ih*min(#{width}/iw\\,#{height}/ih))/2", input, output
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=#{ratio}", input, output
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 scale(width, height, input=nil, output=nil)
147
- inout "scale=iw*min(#{width}/iw\\,#{height}/ih):ih*min(#{width}/iw\\,#{height}/ih)", input, output
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(width, height, _fps, input=nil, output=nil)
177
+ def scale_pad_fps(resolution, _fps, input=nil, output=nil)
151
178
  inout [
152
- *scale(width, height),
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=#{silence_noise_max_db}dB", input, output
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=#{duration}", nil, output
190
+ inout "aevalsrc=0:d=%{duration}", nil, output, duration: duration
165
191
  end
166
192
 
167
- # XXX might be very useful with transitions: def smartblur
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 transition_av(transition, resolution, fps, inputs, output=nil, video: true, audio: true)
174
- blend_duration = transition[:blend].to_f
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 = "rn#{inputs.object_id}" # should be sufficiently random
202
+ aux_lbl = "blnd#{inputs[0]}"
179
203
  auxx_lbl = "x#{aux_lbl}"
180
- [].tap do |filters|
181
- filters.concat [
182
- *white_source(blend_duration, resolution, fps, "#{aux_lbl}:v"),
183
- *inout([
184
- *alphamerge(["#{inputs.first}:v", "#{aux_lbl}:v"]),
185
- *fade_out_alpha(blend_duration)
186
- ].join(', '), nil, "#{auxx_lbl}:v"),
187
- *overlay(0, 0, ["#{inputs.last}:v", "#{auxx_lbl}:v"], "#{output}:v"),
188
- ] if video
189
- filters.concat [
190
- *afade_out(blend_duration, "#{inputs.first}:a", "#{aux_lbl}:a"),
191
- *afade_in(blend_duration, "#{inputs.last}:a", "#{auxx_lbl}:a"),
192
- *amix_to_first_same_volume(["#{auxx_lbl}:a", "#{aux_lbl}:a"], "#{output}:a")
193
- ] if audio
194
- end
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=#{[st, en].compact.join ':'}, setpts=PTS-STARTPTS", input, output
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='#{volume_exp volume}':eval=frame", input, output
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.tap do |f|
240
- f.prepend "#{[*inputs].map{|s| "[#{s}]"}.join ' '} " if inputs
241
- f << " #{[*outputs].map{|s| "[#{s}]"}.join ' '}" if outputs
242
- end
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