ffmprb 0.6.6

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