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 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