your audio/video manipulation pal, based on ffmpeg
A video and audio composing DSL (Damn-Simple Language) and a micro-engine for ffmpeg and ffriends (with CLI)
Any script-able person can manipulate video/audio media -- or automate such processing -- with ffmprb.
ATTENTION
Please see the Dockerfile
for runtime dependencies -- or just build
a (self-contained) container image right away:
- for use on the command line or in shell scripts
- for (Ruby-based) media processing microservices
SYNOPSIS
Allows for scripts like
in_main = input('flick.mp4')
output 'cine.flv', video: {resolution: '1280x720'} do
roll in_main.crop(0.25).cut(from: 2, to: 5), transition: {blend: 1}
roll in_main.volume(2).cut(from: 6, to: 16), after: 2, transition: {blend: 1}
overlay input('track.mp3').volume(0.8)
end
and saves you from the horror of...
ffmpeg -y -noautorotate -i flick.mp4 -i track.mp3 -filter_complex "[0:v] fps=fps=16 [cptmo0rl0:v]; [0:a] anull [cptmo0rl0:a]; [cptmo0rl0:v] crop=x=in_w*0.25:y=in_h*0.25:w=in_w*0.5:h=in_h*0.5 [tmptmo0rl0:v]; [tmptmo0rl0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), setsar=1, pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, setsar=1 [tmo0rl0:v]; [cptmo0rl0:a] anull [tmo0rl0:a]; color=0x000000@0:d=3:s=1280x720:r=16 [blo0rl0:v]; [tmo0rl0:v] [blo0rl0:v] concat=2:v=1:a=0 [pdo0rl0:v]; [pdo0rl0:v] trim=2:5, setpts=PTS-STARTPTS [o0rl0:v]; aevalsrc=0:d=3 [blo0rl0:a]; [tmo0rl0:a] [blo0rl0:a] concat=2:v=0:a=1 [pdo0rl0:a]; [pdo0rl0:a] atrim=2:5, asetpts=PTS-STARTPTS [o0rl0:a]; color=0x000000@0:d=1.0:s=1280x720:r=16 [bl0:v]; aevalsrc=0:d=1.0 [bl0:a]; [bl0:v] trim=0:1.0, setpts=PTS-STARTPTS [o0tm0b:v]; [bl0:a] atrim=0:1.0, asetpts=PTS-STARTPTS [o0tm0b:a]; color=0xFFFFFF@1:d=1.0:s=1280x720:r=16 [blndo0tm0b:v]; [o0tm0b:v] [blndo0tm0b:v] alphamerge, fade=out:d=1.0:alpha=1 [xblndo0tm0b:v]; [o0rl0:v] [xblndo0tm0b:v] overlay=x=0:y=0:eof_action=pass [o0tn0:v]; [o0tm0b:a] afade=out:d=1.0:curve=hsin [blndo0tm0b:a]; [o0rl0:a] afade=in:d=1.0:curve=hsin [xblndo0tm0b:a]; [blndo0tm0b:a] apad [apdblndo0tm0b:a]; [xblndo0tm0b:a] [apdblndo0tm0b:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0tn0:a]; [0:v] scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih), setsar=1, pad=1280:720:(1280-iw*min(1280/iw\,720/ih))/2:(720-ih*min(1280/iw\,720/ih))/2, setsar=1, fps=fps=16 [ldtmo0rl1:v]; [0:a] anull [ldtmo0rl1:a]; [ldtmo0rl1:v] copy [tmo0rl1:v]; [ldtmo0rl1:a] volume='2':eval=frame [tmo0rl1:a]; color=0x000000@0:d=10:s=1280x720:r=16 [blo0rl1:v]; [tmo0rl1:v] [blo0rl1:v] concat=2:v=1:a=0 [pdo0rl1:v]; [pdo0rl1:v] trim=6:16, setpts=PTS-STARTPTS [o0rl1:v]; aevalsrc=0:d=10 [blo0rl1:a]; [tmo0rl1:a] [blo0rl1:a] concat=2:v=0:a=1 [pdo0rl1:a]; [pdo0rl1:a] atrim=6:16, asetpts=PTS-STARTPTS [o0rl1:a]; color=0x000000@0:d=3.0:s=1280x720:r=16 [blo0tn01:v]; aevalsrc=0:d=3.0 [blo0tn01:a]; [o0tn0:v] [blo0tn01:v] concat=2:v=1:a=0 [pdo0tn01:v]; [o0tn0:a] [blo0tn01:a] concat=2:v=0:a=1 [pdo0tn01:a]; [pdo0tn01:v] split [pdo0tn01a:v] [pdo0tn01b:v]; [pdo0tn01:a] asplit [pdo0tn01a:a] [pdo0tn01b:a]; [pdo0tn01a:v] trim=0:2, setpts=PTS-STARTPTS [tmo0tn01a:v]; [pdo0tn01a:a] atrim=0:2, asetpts=PTS-STARTPTS [tmo0tn01a:a]; [pdo0tn01b:v] trim=2:3.0, setpts=PTS-STARTPTS [o0tm1b:v]; [pdo0tn01b:a] atrim=2:3.0, asetpts=PTS-STARTPTS [o0tm1b:a]; color=0xFFFFFF@1:d=1.0:s=1280x720:r=16 [blndo0tm1b:v]; [o0tm1b:v] [blndo0tm1b:v] alphamerge, fade=out:d=1.0:alpha=1 [xblndo0tm1b:v]; [o0rl1:v] [xblndo0tm1b:v] overlay=x=0:y=0:eof_action=pass [o0tn1:v]; [o0tm1b:a] afade=out:d=1.0:curve=hsin [blndo0tm1b:a]; [o0rl1:a] afade=in:d=1.0:curve=hsin [xblndo0tm1b:a]; [blndo0tm1b:a] apad [apdblndo0tm1b:a]; [xblndo0tm1b:a] [apdblndo0tm1b:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0tn1:a]; [tmo0tn01a:v] [o0tn1:v] concat=2:v=1:a=0 [o0o:v]; [tmo0tn01a:a] [o0tn1:a] concat=2:v=0:a=1 [o0o:a]; [1:a] anull [ldo0l0:a]; [ldo0l0:a] volume='0.8':eval=frame [o0l0:a]; [o0o:v] copy [o0o0:v]; [o0l0:a] apad [apdo0l0:a]; [o0o:a] [apdo0l0:a] amix=2:duration=shortest:dropout_transition=0, volume=2 [o0o0:a]" -map "[o0o0:v]" -map "[o0o0:a]" -c:a libmp3lame cine.flv
...that's the idea, but there's much more to it.
The docs, more than other parts of this gem, are a work in progress.
So, if you're really curious about using ffmprb
, you have to check the specs for the actual functionality coverage.
A hint on the ideas you might find there: you can use constructs like
Ffmprb::File.temp_fifo('.flv') do |av_pipe|
thr = Thread.new do
Ffmprb.process(..., av_pipe) do |..., stream_output|
output(stream_output) do
...
end
end
end
begin
Ffmprb.process(av_pipe, ...) do |stream_input, ...|
...
end
ensure
thr.join
end
end
in order to implement process pipelines.
Add this line to your application's Gemfile:
gem 'ffmprb'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ffmprb
The DSL strives to provide for the most common script cases in the most natural way: you just describe what should be shown -- in an action sequence, like the following.
# Play your _episode_ teaser snippet:
lay episode.cut(to: 60), transition: {blend: 3}
# Overlay anything after that with your channel _logo_:
overlay logo.loop.cut(to: 33), after: 3, transition: {blend: 1} # both ways
# Start with rolling some _intro_ flick:
lay intro, transition: {blend: 1}
# Overlay it with some special _badge_ sprite:
overlay badge.loop, at: 1, transition: {burn: 1}
# Show _title_:
lay title, transition: {blend: 2}
# Play some of your _episode_:
lay episode.cut(from: 60, to: 540)
# Oh well, roll some _promo_ material:
lay promo, transition: {pixel: 2}
# Play most of your _episode_:
lay episode.cut(from: 540, to: 1080)
# Roll the _credits_:
overlay credits, at: 1075
# Finish by playing your special _outro_:
lay outro, transition: {blend: 1}
# Fin
The block above is to be given to an Ffmprb.process
call:
Ffmprb.process do
# Play your _episode_ teaser snippet:
lay episode.cut(to: 60), transition: {blend: 3}
...
end
The block runs in the context of a new Ffmprb::Process
, so any instance data shall be passed by value as follows:
Ffmprb.process @episode, @teaser_length do
|episode, teaser_length|
# Play your _episode_ teaser snippet:
lay episode.cut(to: teaser_length), transition: {blend: 3}
...
end
The ffmprb
command-line utility expects a script on its standard input:
# episode_01.ffmprb
output 'episode_01_tease.mpg' do
# Play your _episode_ teaser snippet:
lay input('episode_01.mov').cut(to: 60), transition: {blend: 3}
...
end
$ ffmprb < episode_01.ffmprb
And it can take parameters for the sake of automation convenience:
# episode_make.ffmprb
|episode, logo, intro, badge, title, promo, credits, outro, final|
output final do
# Play your _episode_ teaser snippet:
lay input(episode).cut(to: 60), transition: {blend: 1}
overlay input(logo).loop.cut(to: 24), after: 3, transition: {blend: 1}
lay input(intro).cut(to: 30), transition: {blend: 3}
lay input(title), transition: {blend: 1}
overlay input(badge), transition: {blend: 1}
lay input(episode).cut(from: 60), transition: {blend: 1}
lay input(promo).cut(from: 60), transition: {blend: 1}
lay input(credits), transition: {blend: 3}
lay input(outro), transition: {blend: 3}
end
$ ffmprb ep01raw.mov logo.png intro.avi new_new.gif ep01tit.mov promo.mp4 ep01creds.avi ep01out.mov ep01.mpg < episode_make.ffmprb
The defaults defaults are provided for every possible configuration option (optional options' defaults for the methods below in particular), you're welcome to config anything in your ffmprb scripts.
Anything ruby-valid will work -- the script may be generated on the fly:
transitions = [:blend, :burn, :zoom]
photos.shuffle.each do |photo|
lay photo.loop.cut(to: rand * 3), transition: {transitions.shuffle.first => 1}
end
Inside a process
block, there are input
definitions and output
definitions;
naturally, the latter use the former:
Ffmprb.process do
in_main = input(av_input1)
output(av_output1, video: {resolution: HD_720p, fps: 25}) do
lay in_main.crop(0.05), transition: {blend: 1}
end
end
input
(file, [video: {false | {[auto_rotate:], [fps:]}}], [audio: false])
input
returns a reel.
output
(file, [video: {false | {[resolution:], [fps:]}}], [audio: {false | {[encoder:], [sampling_freq:]}}])
output
also takes a block where you get to use lay
and overlay
methods:
lay
(reel[, after: sec[, transition: {blend: sec}])
lay
renders the reel full screen after the previously lay
ed reel.
overlay
(reel[, at: sec][, duck: :audio])
overlay
is currently functional just for audio reels, sorry.
audio
audio
channels just the audio from the reel.
crop
({ratio | {[top: ratio][, left: ratio][, bottom: ratio][, right: ratio][, width: ratio][, height: ratio]}})
crop
crops the reel frames (e.g. in1.crop(0.1)
will remove 1/10th of the frame from each side)
cut
([from: sec][, to: sec])
cut
cuts the reel from from:
to to:
.
loop
([times])
loop
loops(!) the reel so many times (no times param means maximum times currently possible).
mute
pace
(factor)
pace
changes the reel speed by the given factor
pp
pp
activates (some very simple) image postprocessing
reverse
reverse
reverses the reel (works efficiently with small fragments only)
volume
(ratio)
volume
changes the volume proportionally to the source. mute
mutes.
video
video
channels just the video from the reel.
copy
(reel)
copy
copies the reel's modifier chain onto the given reel.
- Ffmprb is a work in progress, and even more so than Ffmpeg itself; use at your own risk and check thoroughly for production fitness in your project.
- Ffmprb uses threads internally, however, it is not thread-safe interface-wise: you must not share its objects between different threads.
To enable ProcVis support (source), define FFMPRB_PROC_VIS_FIREBASE_URL=my-proc-vis-io
(replace with your Firebase instance) in your running environment and watch the log for `You may view your process visualised at: https://proc-vis-io.firebaseapp.com/?pid=70311657638000 (a sample ProcVis snapshot of a full specs run).
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.
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.
To enable debug logging, define FFMPRB_DEBUG=1
in the running environment.
To enable ffmpeg debug logging, FFMPRB_FFMPEG_DEBUG=1
Generally, avoid using threads, but not at any cost. If you have to use threads -- like they're already in use in the code -- please follow these simple principles:
- A parent thread, when in normal operation, will join all its child threads --
either via
#join
or#value
. - A child thread, when in normal long-running operation, will check on its parent
thread periodically -- probably together with logging/quitting operation itself on timeouts
(either with a use of
Timeout.timeout
or otherwise): if it's dead with exception (status=nil), the child should die with exception as well. - To avoid confusion, do not allow Timeout exception (or other thread-management-related errors) to escape threads (otherwise the joining parent must distinguish between its own timout and that of a joined thread)
- Fork it ( https://github.com/costa/ffmprb/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request