ffmprb

Gem Version

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.

Installation

Add this line to your application's Gemfile:

gem 'ffmprb'

And then execute:

$ bundle

Or install it yourself as:

$ gem install ffmprb

DSL & Usage

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_:
# XXX make this actually work... also, overlay must come before lay
overlay .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

In the code

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

Command line

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

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.

Advanced usage

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

Inputs/outputs

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: | {[auto_rotate:], [fps:]}], [audio: false])

input returns a reel.

output(file, [video: | {[resolution:], [fps:]}], [audio: | {[encoder:], [sampling_freq:]}])

output also takes a block where you get to use lay and overlay methods:

lay(reel[, after: sec[, transition: sec])

lay renders the reel full screen after the previously layed reel.

overlay(reel[, at: sec][, duck: :audio])

overlay is currently functional just for audio reels, sorry.

Available reel modifier (filter) methods

audio

audio channels just the audio from the reel.

crop(| {[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.

Attention

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

ProcVis support (experimental)

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

Development

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.

Debug

To enable debug logging, define FFMPRB_DEBUG=1 in the running environment. To enable ffmpeg debug logging, FFMPRB_FFMPEG_DEBUG=1

Threading policy

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)

Contributing

  1. Fork it ( https://github.com/costa/ffmprb/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request