ffmprb 0.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +12 -0
- data/README.md +72 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/guard +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/ffmprb.gemspec +30 -0
- data/lib/ffmprb/file.rb +184 -0
- data/lib/ffmprb/filter.rb +234 -0
- data/lib/ffmprb/process/input.rb +178 -0
- data/lib/ffmprb/process/output.rb +332 -0
- data/lib/ffmprb/process.rb +98 -0
- data/lib/ffmprb/util/io_buffer.rb +211 -0
- data/lib/ffmprb/util/synchro.rb +47 -0
- data/lib/ffmprb/util/thread.rb +28 -0
- data/lib/ffmprb/util.rb +89 -0
- data/lib/ffmprb/version.rb +3 -0
- data/lib/ffmprb.rb +83 -0
- metadata +181 -0
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
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.2.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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
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
|
data/lib/ffmprb/file.rb
ADDED
@@ -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
|