This library abstracts the complex command-line usage of ffmpeg into a fluent, easy to use node.js module. In order to be able to use this module, make sure you have ffmpeg installed on your system (including all necessary encoding libraries like libmp3lame or libx264).
Now including input streaming support (means you can convert on-the-fly using an input- and an outputstream)!
Via npm:
$ npm install fluent-ffmpeg
Or as a submodule:
$ git submodule add git://github.com/schaermu/node-fluent-ffmpeg.git vendor/fluent-ffmpeg
To run unit tests, first make sure you installed npm dependencies (run npm install
).
$ make test
If you want to re-generate the test coverage report (filed under test/coverage.html), run
$ make test-cov
Make sure your ffmpeg installation is up-to-date to prevent strange assertion errors because of missing codecs/bugfixes.
You will find a lot of usage examples (including a real-time streaming example using flowplayer and express!) in the examples
folder.
fluent-ffmpeg requires ffmpeg >= 0.9 to work. It may work with previous versions but several features won't be available (and the library is not tested with lower versions anylonger).
If the FFMPEG_PATH
environment variable is set, fluent-ffmpeg will use it as the full path to the ffmpeg
executable. Otherwise, it will attempt to call ffmpeg
directly (so it should be in your PATH
). You must also have ffprobe installed (it comes with ffmpeg in most distributions). Similarly, fluent-ffmpeg will use the FFPROBE_PATH
environment variable if it is set, otherwise it will attempt to call it in the PATH
.
Most features should work when using avconv and avprobe instead of ffmpeg and ffprobe, but they are not officially supported at the moment.
Windows users: most probably ffmpeg and ffprobe will not be in your %PATH
, so you must set %FFMPEG_PATH
and %FFPROBE_PATH
.
Debian/Ubuntu users: the official repositories have the ffmpeg/ffprobe executable in the libav-tools
package, and they are actually rebranded avconv/avprobe executables (avconv is a fork of ffmpeg). They should be mostly compatible, but should you encounter any issue, you may want to use the real ffmpeg instead. You can either compile it from source or find a pre-built .deb package at https://ffmpeg.org/download.html (For Ubuntu, the ppa:jon-severinsson/ffmpeg
PPA provides recent builds).
If you intend to encode FLV videos, you must have either flvtool2 or flvmeta installed and in your PATH
or fluent-ffmpeg won't be able to produce streamable output files.
The fluent-ffmpeg module returns a constructor that you can use to instanciate FFmpeg commands. You have to supply a configuration object containing at least the input source.
var FFmpeg = require('fluent-ffmpeg');
var command = new FFmpeg({ source: '/path/to/file.avi' });
The input file can also be an image or an image pattern.
var imageCommand = new FFmpeg({ source: '/path/to/file.png' });
// Will use file000.png, file001.png, etc.
var patternCommand = new FFmpeg({ source: '/path/to/file%03d.png' });
You can also pass a Readable stream instead of a source path.
var inStream = fs.createReadStream('/path/to/file.avi');
var command = new FFmpeg({ source: inStream });
Additional options can be supplied when creating a command.
var command = new FFmpeg({
// Source filename or input stream
source: '/path/to/file.avi',
// Custom presets folder
preset: './presets',
// Processing timeout in seconds, defaults to no timeout
// You can disable the timeout by passing 0.
timeout: 30,
// Priority (niceness) for spawned FFmpeg processes.
// Defaults to 0; not supported on Windows platforms
priority: -5,
// Custom Winston logger instance, defaults to not
// logging anything.
logger: new winston.Logger(...),
// Disable logging, even if logger is specified
nolog: false
});
FFmpeg commands have several methods to pass options to FFmpeg. All these methods return the command object, so that calls can be chained. Here are all the available methods:
new FFmpeg({ source: '/path/to/video.avi' })
/** Stream selection **/
// Skip video streams in output
.withNoVideo()
// Skip audio streams in output
.withNoAudio()
// Add an input file
.addInput('/path/to/audiotrack.mp3')
/** Video codec and bitrate **/
// Specify video codec
.withVideoCodec('libx624')
// Specify video bitrate in kbps (the 'k' is optional)
.withVideoBitrate('650k')
// Specify a constant video bitrate
.withVideoBitrate('650k', true)
/** Video size **/
// Specify a fixed output size
.withSize('320x240')
// Specify a proportional resize of the input
.withSize('50%')
// Force one dimension and automatically determine the other
.withSize('320x?')
.withSize('?x240')
// Auto-pad video, defaulting to black padding
.applyAutopadding(true)
.applyAutopadding(true, 'white')
// Set aspect ratio
.withAspectRatio(1.33)
// Keep aspect ratio
.keepPixelAspect(true)
/** Video timing **/
// Specify input FPS (only valid for raw video formats)
.withFpsInput(24)
// Set output FPS
.withFps(24)
.withFpsOutput(24)
// Loop indefinitely, only relevant when outputting to a stream
.loop()
// Loop for a certain duration in seconds
.loop(120)
// Skip to specific timestamp
.setStartTime(120)
.setStartTime('2:00')
// Only transcode a certain duration
.setDuration(120)
.setDuration('2:00')
// Only transcode a certain number of frames
.takeFrames(250)
/** Audio codec and bitrate **/
// Specify audio codec
.withAudioCodec('libmp3lame')
// Specify audio bitrate in kbps (the 'k' is optional)
.withAudioBitrate('128k')
// Specify the number of audio channels
.withAudioChannels(2)
// Specify the audio sample frequency
.withAudioFrequency(48000)
// Specify the audio quality factor
.withAudioQuality(5)
/** Format options **/
// Specify input format
.fromFormat('avi')
// Set output format
.toFormat('webm')
/** Custom filters **/
// Add custom audio filters
.withAudioFilter('equalizer=f=1000:width_type=h:width=200:g=-10')
.withAudioFilter('pan=1:c0=0.9*c0+0.1*c1')
// Add custom video filters
.withVideoFilter('size=iw*1.5:ih/2')
.withVideoFilter('drawtext=\'fontfile=FreeSans.ttf:text=Hello\'')
/** Miscellaneous options **/
// Use strict experimental flag (needed for some codecs)
.withStrictExperimental()
// Add custom input option (will be added before the input
// on ffmpeg command line)
.addInputOption('-f', 'avi')
// Add several input options at once
.addInputOptions(['-f avi', '-ss 2:30'])
// Add custom option
.addOption('-crf', '23')
// Add several options at once
.addOptions(['-crf 23', '--preset ultrafast']);
You can set events listeners on a command.
var command = new FFmpeg({ source: '/path/to/video.avi' })
.on('start', function(commandLine) {
// The 'start' event is emitted just after the FFmpeg
// process is spawned.
console.log('Spawned FFmpeg with command: ' + commandLine);
})
.on('codecData', function(data) {
// The 'codecData' event is emitted when FFmpeg first
// reports input codec information. 'data' contains
// the following information:
// - 'format': input format
// - 'duration': input duration
// - 'audio': audio codec
// - 'audio_details': audio encoding details
// - 'video': video codec
// - 'video_details': video encoding details
console.log('Input is ' + data.audio + ' audio with ' + data.video + ' video');
})
.on('progress', function(progress) {
// The 'progress' event is emitted every time FFmpeg
// reports progress information. 'progress' contains
// the following information:
// - 'frames': the total processed frame count
// - 'currentFps': the framerate at which FFmpeg is
// currently processing
// - 'currentKbps': the throughput at which FFmpeg is
// currently processing
// - 'targetSize': the current size of the target file
// in kilobytes
// - 'timemark': the timestamp of the current frame
// in seconds
// - 'percent': an estimation of the progress
console.log('Processing: ' + progress.percent + '% done');
})
.on('error', function(err) {
// The 'error' event is emitted when an error occurs,
// either when preparing the FFmpeg process or while
// it is running
console.log('Cannot process video: ' + err.message);
})
.on('end', function() {
// The 'end' event is emitted when FFmpeg finishes
// processing.
console.log('Processing finished successfully');
});
Note that you should always set a listener for the error
event. If you have not set any listener and an error happens, your nodejs process will end.
Use the saveToFile
method on a command to start processing and save the output to a file.
new FFmpeg({ source: })
.withVideoCodec('libx264')
.withAudioCodec('libmp3lame')
.withSize('320x240')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function() {
console.log('Processing finished !');
})
.saveToFile('/path/to/output.mp4');
Use the writeToStream
method on a command to start processing and pipe the output to a writable stream. You can supply pipe options as a second argument to writeToStream
.
var outStream = fs.createWriteStream('/path/to/output.mp4');
new FFmpeg({ source: '/path/to/video.avi' })
.withVideoCodec('libx264')
.withAudioCodec('libmp3lame')
.withSize('320x240')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function() {
console.log('Processing finished !');
})
.writeToStream(outStream, { end: true });
You can define a preset as a function that takes an FfmpegCommand
as an argument and calls method on it, and then pass it to usePreset
.
function myPreset(command) {
command
.withAudioCodec('libmp3lame')
.withVideoCodec('libx264')
.withSize('320x240');
}
new Ffmpeg({ source: '/path/to/video.avi' })
.usingPreset(myPreset)
.saveToFile('/path/to/converted.mp4');
Preset modules are located in fluent-ffmpeg lib/presets
directory. To use a preset, call the usingPreset
method on a command.
new FFmpeg({ source: '/path/to/video.avi' })
.usingPreset('flashvideo')
.saveToFile('/path/to/converted.flv');
Preset modules should export a load
method, which will receive the command object to alter.
exports.load = function(command) {
command
.withAudioCodec('libmp3lame')
.withVideoCodec('libx264')
.withSize('320x240');
};
fluent-ffmpeg comes with the following preset modules preinstalled:
divx
flashvideo
podcast
Use the mergeAdd
and mergeToFile
methods on a command to concatenate multiple inputs to a single output file. The mergeToFile
needs a temporary folder as its second argument.
new FFmpeg({ source: '/path/to/part1.avi' })
.mergeAdd('/path/to/part2.avi')
.mergeAdd('/path/to/part2.avi')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function() {
console.log('Merging finished !');
})
.mergeToFile('/path/to/merged.avi', '/path/to/tempDir');
One pretty neat feature of fluent-ffmpeg is the ability to generate any amount of thumbnails from your movies. The screenshots are taken at automatically determined timemarks using the following formula: (duration_in_sec * 0.9) / number_of_thumbnails
.
When generating thumbnails, the end
event is dispatched with an array of generated filenames as an argument.
new FFmpeg({ source: '/path/to/video.avi' })
.withSize('320x240')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function(filenames) {
console.log('Successfully generated ' + filenames.join(', '));
})
.takeScreenshots(5, '/path/to/directory');
You can also call takeScreenshots
with specific timemarks.
new FFmpeg({ source: '/path/to/video.avi' })
.withSize('320x240')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function(filenames) {
console.log('Successfully generated ' + filenames.join(', '));
})
.takeScreenshots(
{
count: 2,
timemarks: [ '0.5', '1' ]
},
'/path/to/directory'
);
You can set a filename pattern using following format characters:
%s
- offset in seconds%w
- screenshot width%h
- screenshot height%r
- screenshot resolution (eg. '320x240')%f
- input filename%b
- input basename (filename w/o extension)%i
- number of screenshot in timemark array (can be zero-padded by using it like%000i
)
If multiple timemarks are given and no %i
format character is found in filename, _%i
will be added to the end of the given pattern.
new FFmpeg({ source: '/path/to/video.avi' })
.withSize('320x240')
.on('error', function(err) {
console.log('An error occurred: ' + err.message);
})
.on('end', function(filenames) {
console.log('Successfully generated ' + filenames.join(', '));
})
.takeScreenshots(
{
count: 2,
timemarks: [ '0.5', '1' ],
filename: '%b-thumbnail-%i-%r'
},
'/path/to/directory'
);
You can control the spawned FFmpeg process with the kill
and renice
methods. kill
only works after having spawned an FFmpeg process, but renice
can be used at any time.
var command = new FFmpeg({ source: '/path/to/video.avi' })
.withVideoCodec('libx264')
.withAudioCodec('libmp3lame')
// Set initial niceness
.renice(5)
.saveToFile('/path/to/output.mp4');
// Change process niceness (not supported on Windows platforms)
command.renice(-5);
// Send custom signals
command.kill('SIGSTOP');
setTimeout(function() {
command.kill('SIGCONT');
}, 1000);
// `kill` defaults to SIGKILL (will make command emit an 'error' event)
setTimeout(function() {
command.kill();
}, 2000);
You can read metadata from any valid ffmpeg input file with the modules ffprobe
method.
var Ffmpeg = require('fluent-ffmpeg');
Ffmpeg.ffprobe('/path/to/file.avi', function(err, metadata) {
console.dir(metadata);
});
The returned object is the same that is returned by running the following command from your shell:
$ ffprobe -of json -show_streams -show_format /path/to/file.avi
It will contain information about the container (as a format
key) and an array of streams (as a stream
key). The format object and each stream object also contains metadata tags, depending on the format:
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "Constrained Baseline",
"codec_type": "video",
"codec_time_base": "1/48",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 320,
"height": 180,
"has_b_frames": 0,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p",
"level": 13,
"r_frame_rate": "24/1",
"avg_frame_rate": "24/1",
"time_base": "1/24",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 14315,
"duration": "596.458333",
"bit_rate": "702655",
"nb_frames": "14315",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0
},
"tags": {
"creation_time": "1970-01-01 00:00:00",
"language": "und",
"handler_name": "\fVideoHandler"
}
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "48000",
"channels": 2,
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 28619776,
"duration": "596.245333",
"bit_rate": "159997",
"nb_frames": "27949",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0
},
"tags": {
"creation_time": "1970-01-01 00:00:00",
"language": "und",
"handler_name": "\fSoundHandler"
}
}
],
"format": {
"filename": "http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4",
"nb_streams": 2,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"format_long_name": "QuickTime / MOV",
"start_time": "0.000000",
"duration": "596.459000",
"size": "64657027",
"bit_rate": "867211",
"tags": {
"major_brand": "isom",
"minor_version": "512",
"compatible_brands": "mp41",
"creation_time": "1970-01-01 00:00:00",
"title": "Big Buck Bunny",
"artist": "Blender Foundation",
"composer": "Blender Foundation",
"date": "2008",
"encoder": "Lavf52.14.0"
}
}
}
Warning: this method is not very reliable and thus deprecated. Use at your own risk.
Using a separate object, you can access various metadata of your video file.
var Metadata = require('fluent-ffmpeg').Metadata;
new Metadata(
'/path/to/your_movie.avi',
function(metadata, err) {
console.log(require('util').inspect(metadata, false, null));
}
);
fluent-ffmpeg enables you to query your installed ffmpeg version for supported formats, codecs and filters.
var Ffmpeg = require('fluent-ffmpeg');
Ffmpeg.getAvailableFormats(function(err, formats) {
console.log("Available formats:");
console.dir(formats);
});
Ffmpeg.getAvailableCodecs(function(err, codecs) {
console.log("Available codecs:");
console.dir(codecs);
});
Ffmpeg.getAvailableFilters(function(err, filters) {
console.log("Available filters:");
console.dir(filters);
});
// Those methods can also be called on commands
new Ffmpeg({ source: "/path/to/file.avi "})
.getAvailableCodecs(...);
These methods pass an object to their callback with keys for each available format, codec or filter.
The returned object for formats looks like:
{
...
mp4: {
description: 'MP4 (MPEG-4 Part 14)',
canDemux: false,
canMux: true
},
...
}
canDemux
indicates whether ffmpeg is able to extract streams from (demux) this formatcanMux
indicates whether ffmpeg is able to write streams into (mux) this format
The returned object for codecs looks like:
{
...
mp3: {
type: 'audio',
description: 'MP3 (MPEG audio layer 3)',
canDecode: true,
canEncode: true,
intraFrameOnly: false,
isLossy: true,
isLossless: false
},
...
}
type
indicates the codec type, either "audio", "video" or "subtitle"canDecode
tells whether ffmpeg is able to decode streams using this codeccanEncode
tells whether ffmpeg is able to encode streams using this codec
Depending on your ffmpeg version (or if you use avconv instead) other keys may be present, for example:
directRendering
tells if codec can render directly in GPU RAM; useless for transcoding purposesintraFrameOnly
tells if codec can only work with I-framesisLossy
tells if codec can do lossy encoding/decodingisLossless
tells if codec can do lossless encoding/decoding
With some ffmpeg/avcodec versions, the description includes encoder/decoder mentions in the form "Foo codec (decoders: libdecodefoo) (encoders: libencodefoo)". In this case you will want to use those encoders/decoders instead (the codecs object returned by getAvailableCodecs
will also include them).
The returned object for filters looks like:
{
...
scale: {
description: 'Scale the input video to width:height size and/or convert the image format.',
input: 'video',
multipleInputs: false,
output: 'video',
multipleOutputs: false
},
...
}
input
tells the input type this filter operates on, one of "audio", "video" or "none". When "none", the filter likely generates output from nothingmultipleInputs
tells whether the filter can accept multiple inputsoutput
tells the output type this filter generates, one of "audio", "video" or "none". When "none", the filter has no output (sink only)multipleInputs
tells whether the filter can generate multiple outputs
Contributions in any form are highly encouraged and welcome! Be it new or improved presets, optimized streaming code or just some cleanup. So start forking!
(The MIT License)
Copyright (c) 2011 Stefan Schaermeli <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.