ffmprb 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ecadecd6bd073a3c90519fb40212a178f3651adb
4
+ data.tar.gz: d5f7b54d90b83f1411b04e08b33dac3cdc67f767
5
+ SHA512:
6
+ metadata.gz: 9921d1fdef89e73290d7c0b33a5adea2b85e2734fa1cf4438310535b3e0d25e3b81b9f13bfe49d7a2a50632dc669e52c88db181e4a480cf78bb067dbb113e21f
7
+ data.tar.gz: 808e276f8647f11908efba894d3b92777d75f142e6487ffaea846dc4767041ee718f2e7dd6b1f5414d1045bed738813dc63e38763a1e10eb0cdeab8dbd35955a
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --require spec_helper
2
+ --order random
3
+ --format documentation
4
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.1
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ffmprb.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ guard :rspec,
2
+ :cmd => 'bin/rspec',
3
+ :run_all => {:cmd => 'bin/rspec --format documentation --profile'},
4
+ :all_after_pass => false,
5
+ :all_on_start => false,
6
+ :failed_mode => :focus do
7
+
8
+ watch(%r{^spec/.+_spec\.rb$})
9
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
10
+ watch('spec/spec_helper.rb') { 'spec' }
11
+ watch('.rspec') { 'spec' }
12
+ end
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # ffmprb
2
+ ## your audio/video montage friend, based on [ffmpeg](https://ffmpeg.org)
3
+
4
+ A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends.
5
+
6
+ Allows for code like
7
+ ```ruby
8
+ av_raw = Ffmprb::File.open('flick.mp4')
9
+ a_track = Ffmprb::File.open('track.wav')
10
+ av_final = Ffmprb::File.create('cine.flv')
11
+ Ffmprb.process(av_raw, a_track, av_final) do |av_input1, a_input1, av_output1|
12
+
13
+ in_main = input(av_input1)
14
+ in_sound = input(a_input1, only: :audio)
15
+ output(av_output1, resolution: Ffmprb::HD_720p) do
16
+ roll in_main.cut(from: 2, to: 5).crop(0.25), transition: {blend: 1}
17
+ roll in_main.cut(from: 6).volume(2), after: 2, transition: {blend: 1}
18
+ cut after: 10, transition: {blend: 1}
19
+ overlay in_sound.volume(0.8), duck: :audio
20
+ end
21
+
22
+ end
23
+ ```
24
+ and saves you from the horrors of (the native ffmpeg equivalent)
25
+ ```
26
+ ffmpeg -y -i flick.mp4 -i track.wav -filter_complex "[0:v] copy [tmcpsp0:v]; [0:a] anull [tmcpsp0:a]; [tmcpsp0:v] trim=2:5, setpts=PTS-STARTPTS [cpsp0:v]; [tmcpsp0:a] atrim=2:5, asetpts=PTS-STARTPTS [cpsp0:a]; [cpsp0:v] crop=x=in_w*0.25:y=in_h*0.25:w=in_w*0.5:h=in_h*0.5 [sp0:v]; [cpsp0: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]; color=black:d=1.0:s=1280x720:r=30 [bl0:v]; aevalsrc=0:d=1.0 [bl0:a]; [bl0:v] trim=0:1.0, setpts=PTS-STARTPTS [tm0b:v]; [bl0:a] atrim=0:1.0, asetpts=PTS-STARTPTS [tm0b:a]; color=white:d=1.0:s=1280x720:r=30 [rn70152323222540:v]; [tm0b:v] [rn70152323222540:v] alphamerge, fade=out:d=1.0:alpha=1 [xrn70152323222540:v]; [rl0:v] [xrn70152323222540:v] overlay=x=0:y=0:eof_action=pass [tn0:v]; [tm0b:a] afade=out:d=1.0 [rn70152323222540:a]; [rl0:a] afade=in:d=1.0 [xrn70152323222540:a]; [xrn70152323222540:a] [rn70152323222540:a] amix=2:duration=first [tn0:a]; [0:v] copy [tmldsp1:v]; [0:a] anull [tmldsp1:a]; [tmldsp1:v] trim=6, setpts=PTS-STARTPTS [ldsp1:v]; [tmldsp1:a] atrim=6, asetpts=PTS-STARTPTS [ldsp1:a]; [ldsp1:v] copy [sp1:v]; [ldsp1:a] volume='2':eval=frame [sp1:a]; [sp1: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 [rl1:v]; [sp1:a] anull [rl1:a]; color=black:d=3.0:s=1280x720:r=30 [bltn01:v]; aevalsrc=0:d=3.0 [bltn01:a]; [tn0:v] [bltn01:v] concat=2:v=1:a=0 [pdtn01:v]; [tn0:a] [bltn01:a] concat=2:v=0:a=1 [pdtn01:a]; [pdtn01:v] split [pdtn01a:v] [pdtn01b:v]; [pdtn01:a] asplit [pdtn01a:a] [pdtn01b:a]; [pdtn01a:v] trim=0:2, setpts=PTS-STARTPTS [tmtn01a:v]; [pdtn01a:a] atrim=0:2, asetpts=PTS-STARTPTS [tmtn01a:a]; [pdtn01b:v] trim=2:3.0, setpts=PTS-STARTPTS [tm1b:v]; [pdtn01b:a] atrim=2:3.0, asetpts=PTS-STARTPTS [tm1b:a]; color=white:d=1.0:s=1280x720:r=30 [rn70152323237760:v]; [tm1b:v] [rn70152323237760:v] alphamerge, fade=out:d=1.0:alpha=1 [xrn70152323237760:v]; [rl1:v] [xrn70152323237760:v] overlay=x=0:y=0:eof_action=pass [tn1:v]; [tm1b:a] afade=out:d=1.0 [rn70152323237760:a]; [rl1:a] afade=in:d=1.0 [xrn70152323237760:a]; [xrn70152323237760:a] [rn70152323237760:a] amix=2:duration=first [tn1:a]; color=black:d=11.0:s=1280x720:r=30 [bltn12:v]; aevalsrc=0:d=11.0 [bltn12:a]; [tn1:v] [bltn12:v] concat=2:v=1:a=0 [pdtn12:v]; [tn1:a] [bltn12:a] concat=2:v=0:a=1 [pdtn12:a]; [pdtn12:v] split [pdtn12a:v] [pdtn12b:v]; [pdtn12:a] asplit [pdtn12a:a] [pdtn12b:a]; [pdtn12a:v] trim=0:10, setpts=PTS-STARTPTS [tmtn12a:v]; [pdtn12a:a] atrim=0:10, asetpts=PTS-STARTPTS [tmtn12a:a]; color=black:d=1.0:s=1280x720:r=30 [bk2:v]; aevalsrc=0:d=1.0 [bk2:a]; [pdtn12b:v] trim=10:11.0, setpts=PTS-STARTPTS [tm2b:v]; [pdtn12b:a] atrim=10:11.0, asetpts=PTS-STARTPTS [tm2b:a]; color=white:d=1.0:s=1280x720:r=30 [rn70152323255640:v]; [tm2b:v] [rn70152323255640:v] alphamerge, fade=out:d=1.0:alpha=1 [xrn70152323255640:v]; [bk2:v] [xrn70152323255640:v] overlay=x=0:y=0:eof_action=pass [tn2:v]; [tm2b:a] afade=out:d=1.0 [rn70152323255640:a]; [bk2:a] afade=in:d=1.0 [xrn70152323255640:a]; [xrn70152323255640:a] [rn70152323255640:a] amix=2:duration=first [tn2:a]; [tmtn01a:v] [tmtn12a:v] [tn2:v] concat=3:v=1:a=0 [oo:v]; [tmtn01a:a] [tmtn12a:a] [tn2:a] concat=3:v=0:a=1 [oo:a]; [1:a] anull [ldol0:a]; [ldol0:a] volume='0.8':eval=frame [ol0:a]" -map "[oo:v]" -map "[oo:a]" /tmp/inter1a.flv -map "[ol0:a]" /tmp/inter1b.wav
27
+ ```
28
+ then some scripting around
29
+ ```
30
+ ffmpeg -y -i /tmp/inter1a.flv -filter_complex "silencedetect=d=2:n=-30dB" /tmp/inter2.flv
31
+ ```
32
+ and finally
33
+ ```
34
+ ffmpeg -y -i /tmp/inter2.flv -i /tmp/inter1b.wav -filter_complex "[0:v] copy [sp0:v]; [0:a] anull [sp0:a]; [sp0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, fps=fps=30 [rl0:v]; [sp0:a] anull [rl0:a]; [rl0:v] concat=1:v=1:a=0 [oo:v]; [rl0:a] concat=1:v=0:a=1 [oo:a]; [1:a] anull [ldol0:a]; [ldol0:a] volume='if(between(t, 9.5, 10.5), (-0.8*t + 8.500000000000002)/1.0, if(between(t, 0.5, 9.5), 0.9, if(between(t, -0.5, 0.5), (0.8*t + 0.5)/1.0, if(between(t, 0.0, -0.5), 0.1, if(between(t, 0.0, 0.0), 0.1, 0.1)))))':eval=frame [ol0:a]; [oo:v] copy [oo0:v]; [oo:a] [ol0:a] amix=2:duration=first [oo0:a]" -map "[oo0:v]" -map "[oo0:a]" cine.flv
35
+ ```
36
+ Umm... That's the idea.
37
+
38
+
39
+ ## Installation
40
+
41
+ Add this line to your application's Gemfile:
42
+
43
+ ```ruby
44
+ gem 'ffmprb'
45
+ ```
46
+
47
+ And then execute:
48
+
49
+ $ bundle
50
+
51
+ Or install it yourself as:
52
+
53
+ $ gem install ffmprb
54
+
55
+ ## DSL & Usage
56
+
57
+ TODO: Write usage instructions here
58
+
59
+
60
+ ## Development
61
+
62
+ 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.
63
+
64
+ 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).
65
+
66
+ ## Contributing
67
+
68
+ 1. Fork it ( https://github.com/showbox-oss/ffmprb/fork )
69
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
70
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
71
+ 4. Push to the branch (`git push origin my-new-feature`)
72
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ rescue LoadError
7
+ end
8
+
9
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ffmprb"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/guard ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'guard' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('guard', 'guard')
data/bin/rake ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'rake' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('rake', 'rake')
data/bin/rspec ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'rspec' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/ffmprb.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ffmprb/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ffmprb'
8
+ spec.version = Ffmprb::VERSION
9
+ spec.authors = ["showbox.com", "Costa Shapiro @ Showbox"]
10
+ spec.email = ['costa@showbox.com']
11
+
12
+ spec.summary = "ffmprb is your audio/video montage friend, based on https://ffmpeg.org"
13
+ spec.description = "A DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends"
14
+ spec.homepage = 'https://github.com/showbox-oss/ffmprb'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
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
22
+
23
+ spec.add_development_dependency 'bundler', '>= 1.9.9'
24
+ spec.add_development_dependency 'byebug', '>= 4.0.5'
25
+ spec.add_development_dependency 'guard-rspec', '>= 2.12.8'
26
+ spec.add_development_dependency 'rake', '>= 10.4.2'
27
+ spec.add_development_dependency 'rmagick', '>= 2.15'
28
+ spec.add_development_dependency 'rspec', '>= 3.2.0'
29
+ spec.add_development_dependency 'ruby-sox', '>= 0.0.3'
30
+ end
@@ -0,0 +1,184 @@
1
+ require 'json'
2
+ require 'mkfifo'
3
+ require 'tempfile'
4
+
5
+ module Ffmprb
6
+
7
+ class File
8
+
9
+ class << self
10
+
11
+ def buffered_fifo(extname='.tmp')
12
+ input_fifo_file = temp_fifo(extname)
13
+ output_fifo_file = temp_fifo(extname)
14
+
15
+ Ffmprb.logger.debug "Opening #{input_fifo_file.path}>#{output_fifo_file.path} for buffering"
16
+ buff = Util::IoBuffer.new(
17
+ ->{
18
+ Ffmprb.logger.debug "Trying to open #{input_fifo_file.path} for reading+buffering"
19
+ ::File.open(input_fifo_file.path, 'r')
20
+ },
21
+ ->{
22
+ Ffmprb.logger.debug "Trying to open #{output_fifo_file.path} for buffering+writing"
23
+ ::File.open(output_fifo_file.path, 'w')
24
+ }
25
+ )
26
+ thr = Util::Thread.new do
27
+ buff.flush!
28
+ Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} ended"
29
+ input_fifo_file.remove
30
+ output_fifo_file.remove
31
+ end
32
+ Ffmprb.logger.debug "IoBuffering from #{input_fifo_file.path} to #{output_fifo_file.path} started"
33
+
34
+ yield buff if block_given? # XXX a hidden option
35
+
36
+ OpenStruct.new in: input_fifo_file, out: output_fifo_file, thr: thr
37
+ end
38
+
39
+ def create(path)
40
+ new(path: path, mode: :write).tap do |file|
41
+ Ffmprb.logger.debug "Created file with path: #{file.path}"
42
+ end
43
+ end
44
+
45
+ def open(path)
46
+ new(path: (path.respond_to?(:path)? path.path : path), mode: :read).tap do |file|
47
+ Ffmprb.logger.debug "Opened file with path: #{file.path}"
48
+ end
49
+ end
50
+
51
+ def temp(extname)
52
+ file = create(Tempfile.new(['', extname]))
53
+ Ffmprb.logger.debug "Created temp file with path: #{file.path}"
54
+
55
+ return file unless block_given?
56
+
57
+ begin
58
+ yield file
59
+ ensure
60
+ begin
61
+ FileUtils.remove_entry file.path
62
+ rescue
63
+ Ffmprb.logger.warn "Error removing temp file with path #{file.path}: #{$!.message}"
64
+ end
65
+ Ffmprb.logger.debug "Removed temp file with path: #{file.path}"
66
+ end
67
+ end
68
+
69
+ def temp_fifo(extname='.tmp', &blk)
70
+ fifo_file = create(temp_fifo_path extname)
71
+ ::File.mkfifo fifo_file.path
72
+
73
+ return fifo_file unless block_given?
74
+
75
+ begin
76
+ yield
77
+ ensure
78
+ fifo_file.remove
79
+ end
80
+ end
81
+
82
+ def temp_fifo_path(extname)
83
+ ::File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('', 'p' + extname)
84
+ end
85
+
86
+ end
87
+
88
+
89
+ def initialize(path:, mode:)
90
+ @path = path
91
+ @path.close if @path && @path.respond_to?(:close) # NOTE specially for temp files
92
+ path! # NOTE early (exception) raiser
93
+ @mode = mode.to_sym
94
+ raise Error, "Open for read, create for write, ??? for #{@mode}" unless %i[read write].include?(@mode)
95
+ end
96
+
97
+ def path
98
+ path!
99
+ end
100
+
101
+ # Info
102
+
103
+ def extname
104
+ ::File.extname path
105
+ end
106
+
107
+ def length
108
+ @duration ||= probe['format']['duration']
109
+ return @duration.to_f if @duration
110
+
111
+ @duration = probe(true)['frames'].reduce(0){|sum, frame| sum + frame['pkt_duration_time'].to_f}
112
+ end
113
+
114
+ def resolution
115
+ v_stream = probe['streams'].first
116
+ "#{v_stream['width']}x#{v_stream['height']}"
117
+ end
118
+
119
+
120
+ def sample( # NOTE can snap output (an image) or audio (a sound) or both
121
+ at: 0.01,
122
+ video: true,
123
+ audio: nil
124
+ )
125
+ audio = File.temp('.mp3') if audio == true
126
+ video = File.temp('.jpg') if video == true
127
+
128
+ Ffmprb.logger.debug "Snap shooting files, video path: #{video ? video.path : 'NONE'}, audio path: #{audio ? audio.path : 'NONE'}"
129
+
130
+ raise Error, "Incorrect output extname (must be .jpg)" unless !video || video.extname =~ /jpe?g$/
131
+ raise Error, "Incorrect audio extname (must be .mp3)" unless !audio || audio.extname =~ /mp3$/
132
+ raise Error, "Can sample either video OR audio UNLESS a block is given" unless block_given? || (!!audio != !!video)
133
+
134
+ cmd = " -i #{path}"
135
+ cmd << " -deinterlace -an -ss #{at} -r 1 -vcodec mjpeg -f mjpeg #{video.path}" if video
136
+ cmd << " -vn -ss #{at} -t 1 -f mp3 #{audio.path}" if audio
137
+ Util.ffmpeg cmd
138
+
139
+ return video || audio unless block_given?
140
+
141
+ begin
142
+ yield *[video || nil, audio || nil].compact
143
+ ensure
144
+ begin
145
+ video.remove if video
146
+ audio.remove if audio
147
+ Ffmprb.logger.debug "Removed sample files"
148
+ rescue
149
+ Ffmprb.logger.warn "Error removing sample files: #{$!.message}"
150
+ end
151
+ end
152
+ end
153
+
154
+ # Manipulation
155
+
156
+ def remove
157
+ FileUtils.remove_entry path
158
+ Ffmprb.logger.debug "Removed file with path: #{path}"
159
+ @path = nil
160
+ end
161
+
162
+ private
163
+
164
+ def path!
165
+ ( # NOTE specially for temp files
166
+ @path.respond_to?(:path)? @path.path : @path
167
+ ).tap do |path|
168
+ # XXX ensure readabilty/writability/readiness
169
+ raise Error, "'#{path}' is un#{@mode.to_s[0..3]}able" unless path && !path.empty?
170
+ end
171
+ end
172
+
173
+ def probe(force=false)
174
+ return @probe unless !@probe || force
175
+ cmd = " -v quiet -i #{path} -print_format json -show_format -show_streams"
176
+ cmd << " -show_frames" if force
177
+ @probe = JSON.parse(Util::ffprobe cmd).tap do |probe|
178
+ raise Error, "This doesn't look like a ffprobable file" unless probe['streams']
179
+ end
180
+ end
181
+
182
+ end
183
+
184
+ end
@@ -0,0 +1,234 @@
1
+ module Ffmprb
2
+
3
+ module Filter
4
+
5
+ class << self
6
+
7
+ def alphamerge(inputs, output=nil)
8
+ inout "alphamerge", inputs, output
9
+ end
10
+
11
+ def afade_in(duration=1, input=nil, output=nil)
12
+ inout "afade=in:d=#{duration}", input, output
13
+ end
14
+
15
+ def afade_out(duration=1, input=nil, output=nil)
16
+ inout "afade=out:d=#{duration}", input, output
17
+ end
18
+
19
+ # NOTE inputs order matters
20
+ def amix(inputs, output=nil)
21
+ inout "amix=#{[*inputs].length}:duration=first", inputs, output
22
+ end
23
+
24
+ def anull(input=nil, output=nil)
25
+ inout "anull", input, output
26
+ end
27
+
28
+ def anullsink(input=nil)
29
+ inout "anullsink", input, nil
30
+ end
31
+
32
+ def asplit(inputs=nil, outputs=nil)
33
+ inout "asplit", inputs, outputs
34
+ end
35
+
36
+ def atrim(st, en, input=nil, output=nil)
37
+ inout "atrim=#{[st, en].compact.join ':'}, asetpts=PTS-STARTPTS", input, output
38
+ end
39
+
40
+ def black_source(duration, resolution=nil, fps=nil, output=nil)
41
+ filter = "color=black:d=#{duration}"
42
+ filter << ":s=#{resolution}" if resolution
43
+ filter << ":r=#{fps}" if fps
44
+ inout filter, nil, output
45
+ end
46
+
47
+ def fade_out_alpha(duration=1, input=nil, output=nil)
48
+ inout "fade=out:d=#{duration}:alpha=1", input, output
49
+ end
50
+
51
+ def fps(fps, input=nil, output=nil)
52
+ inout "fps=fps=#{fps}", input, output
53
+ end
54
+
55
+ def concat_v(inputs, output=nil)
56
+ inout "concat=#{[*inputs].length}:v=1:a=0", inputs, output
57
+ end
58
+
59
+ def concat_a(inputs, output=nil)
60
+ inout "concat=#{[*inputs].length}:v=0:a=1", inputs, output
61
+ end
62
+
63
+ def concat_av(inputs, output=nil)
64
+ inout "concat=#{inputs.length/2}:v=1:a=1", inputs, output
65
+ end
66
+
67
+ def copy(input=nil, output=nil)
68
+ inout "copy", input, output
69
+ end
70
+
71
+ def crop(crop, input=nil, output=nil)
72
+ inout "crop=#{crop_exps(crop).join ':'}", input, output
73
+ end
74
+
75
+ def crop_exps(crop)
76
+ exps = []
77
+
78
+ if crop[:left] > 0
79
+ exps << "x=in_w*#{crop[:left]}"
80
+ end
81
+
82
+ if crop[:top] > 0
83
+ exps << "y=in_h*#{crop[:top]}"
84
+ end
85
+
86
+ if crop[:right] > 0 && crop[:left]
87
+ raise Error, "Must specify two of {left, right, width} at most" if crop[:width]
88
+ crop[:width] = 1 - crop[:right] - crop[:left]
89
+ elsif crop[:width] > 0
90
+ if !crop[:left] && crop[:right] > 0
91
+ crop[:left] = 1 - crop[:width] - crop[:right]
92
+ exps << "x=in_w*#{crop[:left]}"
93
+ end
94
+ end
95
+ exps << "w=in_w*#{crop[:width]}"
96
+
97
+ if crop[:bottom] > 0 && crop[:top]
98
+ raise Error, "Must specify two of {top, bottom, height} at most" if crop[:height]
99
+ crop[:height] = 1 - crop[:bottom] - crop[:top]
100
+ elsif crop[:height] > 0
101
+ if !crop[:top] && crop[:bottom] > 0
102
+ crop[:top] = 1 - crop[:height] - crop[:bottom]
103
+ exps << "y=in_h*#{crop[:top]}"
104
+ end
105
+ end
106
+ exps << "h=in_h*#{crop[:height]}"
107
+
108
+ exps
109
+ end
110
+
111
+ # XXX might be very useful with UGC: def cropdetect
112
+
113
+ def nullsink(input=nil)
114
+ inout "nullsink", input, nil
115
+ end
116
+
117
+ def overlay(x=0, y=0, inputs=nil, output=nil)
118
+ inout "overlay=x=#{x}:y=#{y}:eof_action=pass", inputs, output
119
+ end
120
+
121
+ def pad(width, height, input=nil, output=nil)
122
+ inout "pad=#{width}:#{height}:(#{width}-iw*min(#{width}/iw\\,#{height}/ih))/2:(#{height}-ih*min(#{width}/iw\\,#{height}/ih))/2", input, output
123
+ end
124
+
125
+ def scale(width, height, input=nil, output=nil)
126
+ inout "scale=iw*min(#{width}/iw\\,#{height}/ih):ih*min(#{width}/iw\\,#{height}/ih)", input, output
127
+ end
128
+
129
+ def scale_pad_fps(width, height, fps, input=nil, output=nil)
130
+ inout [
131
+ *scale(width, height),
132
+ *pad(width, height),
133
+ *fps(fps)
134
+ ].join(', '), input, output
135
+ end
136
+
137
+ def silencedetect(input=nil, output=nil)
138
+ inout "silencedetect=d=2:n=-30dB", input, output
139
+ end
140
+
141
+ def silent_source(duration, output=nil)
142
+ inout "aevalsrc=0:d=#{duration}", nil, output
143
+ end
144
+
145
+ # XXX might be very useful with transitions: def smartblur
146
+
147
+ def split(inputs=nil, outputs=nil)
148
+ inout "split", inputs, outputs
149
+ end
150
+
151
+ def transition_av(transition, resolution, fps, inputs, output=nil, video: true, audio: true)
152
+ blend_duration = transition[:blend].to_f
153
+ raise "Unsupported (yet) transition, sorry." unless
154
+ transition.size == 1 && blend_duration > 0
155
+
156
+ aux_lbl = "rn#{inputs.object_id}" # should be sufficiently random
157
+ auxx_lbl = "x#{aux_lbl}"
158
+ [].tap do |filters|
159
+ filters.concat [
160
+ *white_source(blend_duration, resolution, fps, "#{aux_lbl}:v"),
161
+ *inout([
162
+ *alphamerge(["#{inputs.first}:v", "#{aux_lbl}:v"]),
163
+ *fade_out_alpha(blend_duration)
164
+ ].join(', '), nil, "#{auxx_lbl}:v"),
165
+ *overlay(0, 0, ["#{inputs.last}:v", "#{auxx_lbl}:v"], "#{output}:v"),
166
+ ] if video
167
+ filters.concat [
168
+ *afade_out(blend_duration, "#{inputs.first}:a", "#{aux_lbl}:a"),
169
+ *afade_in(blend_duration, "#{inputs.last}:a", "#{auxx_lbl}:a"),
170
+ *amix(["#{auxx_lbl}:a", "#{aux_lbl}:a"], "#{output}:a")
171
+ ] if audio
172
+ end
173
+ end
174
+
175
+ def trim(st, en, input=nil, output=nil)
176
+ inout "trim=#{[st, en].compact.join ':'}, setpts=PTS-STARTPTS", input, output
177
+ end
178
+
179
+ def volume(volume, input=nil, output=nil)
180
+ inout "volume='#{volume_exp volume}':eval=frame", input, output
181
+ end
182
+
183
+ def volume_exp(volume) # NOTE volume is an ordered Hash
184
+ return volume unless volume.respond_to?(:each)
185
+ raise Error, "volume cannot be empty" if volume.empty?
186
+
187
+ prev_at = 0.0
188
+ prev_vol = volume[prev_at] || 1.0
189
+ exp = "#{volume[volume.keys.last]}"
190
+ volume.each do |at, vol|
191
+ vol_exp =
192
+ if (vol - prev_vol).abs < 0.001
193
+ vol
194
+ else
195
+ "(#{vol - prev_vol}*t + #{prev_vol*at - vol*prev_at})/#{at - prev_at}"
196
+ end
197
+ exp = "if(between(t, #{prev_at}, #{at}), #{vol_exp}, #{exp})"
198
+ prev_at = at
199
+ prev_vol = vol
200
+ end
201
+ exp
202
+ end
203
+
204
+ def white_source(duration, resolution=nil, fps=nil, output=nil)
205
+ filter = "color=white:d=#{duration}"
206
+ filter << ":s=#{resolution}" if resolution
207
+ filter << ":r=#{fps}" if fps
208
+ inout filter, nil, output
209
+ end
210
+
211
+ def complex_options(filters)
212
+ if filters.empty?
213
+ ''
214
+ else
215
+ " -filter_complex \"#{filters.join '; '}\""
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ def inout(filter, inputs, outputs)
222
+ [
223
+ filter.tap do |f|
224
+ f.prepend "#{[*inputs].map{|s| "[#{s}]"}.join ' '} " if inputs
225
+ f << " #{[*outputs].map{|s| "[#{s}]"}.join ' '}" if outputs
226
+ end
227
+ ]
228
+ end
229
+
230
+ end
231
+
232
+ end
233
+
234
+ end