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