Repository: fluent-ffmpeg/node-fluent-ffmpeg Branch: master Commit: 7a701f560f6c Files: 110 Total size: 896.4 KB Directory structure: gitextract_71m5k7op/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── doc/ │ ├── FfmpegCommand.html │ ├── audio.js.html │ ├── capabilities.js.html │ ├── custom.js.html │ ├── ffprobe.js.html │ ├── fluent-ffmpeg.js.html │ ├── global.html │ ├── index.html │ ├── inputs.js.html │ ├── misc.js.html │ ├── options_audio.js.html │ ├── options_custom.js.html │ ├── options_inputs.js.html │ ├── options_misc.js.html │ ├── options_output.js.html │ ├── options_video.js.html │ ├── options_videosize.js.html │ ├── output.js.html │ ├── processor.js.html │ ├── recipes.js.html │ ├── scripts/ │ │ ├── linenumber.js │ │ └── prettify/ │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ ├── styles/ │ │ ├── jsdoc-default.css │ │ ├── prettify-jsdoc.css │ │ └── prettify-tomorrow.css │ ├── utils.js.html │ ├── video.js.html │ └── videosize.js.html ├── examples/ │ ├── any-to-mp4-steam.js │ ├── express-stream.js │ ├── flowplayer/ │ │ ├── flowplayer.controls.swf │ │ ├── flowplayer.swf │ │ └── index.html │ ├── full.js │ ├── image2video.js │ ├── input-stream.js │ ├── livertmp2hls.js │ ├── mergeVideos.js │ ├── metadata.js │ ├── preset.js │ ├── progress.js │ ├── stream.js │ └── thumbnails.js ├── index.js ├── lib/ │ ├── capabilities.js │ ├── ffprobe.js │ ├── fluent-ffmpeg.js │ ├── options/ │ │ ├── audio.js │ │ ├── custom.js │ │ ├── inputs.js │ │ ├── misc.js │ │ ├── output.js │ │ ├── video.js │ │ └── videosize.js │ ├── presets/ │ │ ├── divx.js │ │ ├── flashvideo.js │ │ └── podcast.js │ ├── processor.js │ ├── recipes.js │ └── utils.js ├── package.json ├── test/ │ ├── aliases.test.js │ ├── args.test.js │ ├── assets/ │ │ ├── ffserver.conf │ │ ├── presets/ │ │ │ └── custompreset.js │ │ ├── te[s]t_ video ' _ .flv │ │ ├── teststream.ffm │ │ └── testvideo-5m.mpg │ ├── capabilities.test.js │ ├── helpers.js │ ├── metadata.test.js │ ├── processor.test.js │ └── utils.test.js └── tools/ ├── jsdoc-aliases.js ├── jsdoc-conf.json └── jsdoc-template/ ├── README.md ├── publish.js ├── static/ │ ├── scripts/ │ │ ├── linenumber.js │ │ └── prettify/ │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ └── styles/ │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css └── tmpl/ ├── aliases.tmpl ├── container.tmpl ├── details.tmpl ├── example.tmpl ├── examples.tmpl ├── exceptions.tmpl ├── layout.tmpl ├── mainpage.tmpl ├── members.tmpl ├── method.tmpl ├── params.tmpl ├── properties.tmpl ├── returns.tmpl ├── source.tmpl ├── tutorial.tmpl └── type.tmpl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # How to contribute ## Reporting issues Please start by [reading the FAQ][faq] first. If your question is not answered, here are some guidelines on how to effectively report issues. ### Required information When reporting issues be sure to include at least: * Some code that may be used to reproduce the problem * Which version of fluent-ffmpeg, of ffmpeg and which OS you're using * If the problem only happens with some input sources, please include a link to a source that may be used to reproduce the problem * Be sure to include the full error message when there is one * When an ffmpeg error happens (eg. 'ffmpeg exited with code 1'), you should also include the full output from ffmpeg (stdout and stderr), as it may contain useful information about what whent wrong. You can do that by looking at the 2nd and 3rd parameters of the `error` event handler on an FfmpegCommand, for example: ```js ffmpeg('some/input.mp4') .on('error', function(err, stdout, stderr) { console.log('An error happened: ' + err.message); console.log('ffmpeg standard output:\n' + stdout); console.log('ffmpeg standard error:\n' + stderr); }); ``` ### Ffmpeg usage If your command ends up with an ffmpeg error (eg. 'ffmpeg exited with code X : ...'), be sure to try the command manually from command line. You can get the ffmpeg command line that is executed for a specific Fluent-ffmpeg command by using the `start` event: ```js ffmpeg('some/input.mp4') .on('start', function(cmdline) { console.log('Command line: ' + cmdline); }) ... ``` If it does not work, you most likely have a ffmpeg-related problem that does not fit as a fluent-ffmpeg issue; in that case head to the [ffmpeg documentation](ffmpeg.org/documentation.html) to find out what you did wrong. If it _does_ work, please double-check how you escaped arguments and options when passing them to fluent-ffmpeg. For example, when running from command line, you may have to quote arguments to tell your shell that a space is indeed part of a filename or option. When using fluent-ffmpeg, you don't have to do that, as you're already passing options and arguments separately. Here's a (dumb) example: ```sh $ ffmpeg -i video with spaces.avi Cannot find "video", or unknown option "with", etc... $ ffmpeg -i "video with spaces.avi" Works ``` ```js // Works ffmpeg('video with spaces.avi')...; // Fails, looks for a file with actual double quotes in its name ffmpeg('"video with spaces.avi"')...; ``` [faq]: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/wiki/FAQ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ### Version information * fluent-ffmpeg version: * ffmpeg version: * OS: ### Code to reproduce ```js ``` (note: if the problem only happens with some inputs, include a link to such an input file) ### Expected results ### Observed results ### Checklist * [ ] I have read the [FAQ](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/wiki/FAQ) * [ ] I tried the same with command line ffmpeg and it works correctly (hint: if the problem also happens this way, this is an ffmpeg problem and you're not reporting it to the right place) * [ ] I have included full stderr/stdout output from ffmpeg ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI Testing on: pull_request: push: branches: - master jobs: test: name: Run tests runs-on: ubuntu-latest strategy: matrix: node: [18, 20, 21] steps: - name: Checkout uses: actions/checkout@v4 - name: Install flvtool2 run: sudo gem install flvtool2 - name: Install ffmpeg run: sudo apt install -y ffmpeg - name: Setup node uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} cache: yarn - name: Install dependencies run: yarn - name: Run tests run: yarn test - name: Generate coverage report run: yarn coverage - name: Store coveralls coverage uses: coverallsapp/github-action@v2 with: flag-name: linux-node-${{ matrix.node }} parallel: true - name: Upload to codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: files: coverage/lcov.info name: ubuntu-latest-node-${{ matrix.node }} upload-coverage: name: Upload coverage needs: test if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Upload to coveralls uses: coverallsapp/github-action@v2 with: parallel-finished: true carryforward: "linux-node-18,linux-node-20,linux-node-21" ================================================ FILE: .gitignore ================================================ *.project node_modules .nyc_output *.swp .idea *.iml coverage ================================================ FILE: .npmignore ================================================ *.md .git* test/ examples/ ================================================ FILE: LICENSE ================================================ (The MIT License) Copyright (c) 2011-2015 The fluent-ffmpeg contributors 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. ================================================ FILE: Makefile ================================================ REPORTER = spec MOCHA = node_modules/.bin/mocha test: @NODE_ENV=test $(MOCHA) --require should --reporter $(REPORTER) test-colors: @NODE_ENV=test $(MOCHA) --require should --reporter $(REPORTER) --colors publish: @npm version patch -m "version bump" @npm publish JSDOC = node_modules/.bin/jsdoc JSDOC_CONF = tools/jsdoc-conf.json doc: $(JSDOC) --configure $(JSDOC_CONF) .PHONY: test test-colors publish doc ================================================ FILE: README.md ================================================ # Fluent ffmpeg-API for node.js [![Coverage Status](https://coveralls.io/repos/github/fluent-ffmpeg/node-fluent-ffmpeg/badge.svg?branch=master)](https://coveralls.io/github/fluent-ffmpeg/node-fluent-ffmpeg?branch=master) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffluent-ffmpeg%2Fnode-fluent-ffmpeg.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffluent-ffmpeg%2Fnode-fluent-ffmpeg?ref=badge_shield) ## Fluent-ffmpeg is now deprecated This library is **no longer maintained** and no longer works properly with recent ffmpeg versions. Use it as your own risk, this repository is readonly and does not accept issues or PRs. ## About 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](http://www.ffmpeg.org) installed on your system (including all necessary encoding libraries like libmp3lame or libx264). > This is the documentation for fluent-ffmpeg 2.x. > You can still access the code and documentation for fluent-ffmpeg 1.7 [here](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/tree/1.x). ## Installation Via npm: ```sh $ npm install fluent-ffmpeg ``` Or as a submodule: ```sh $ git submodule add git://github.com/schaermu/node-fluent-ffmpeg.git vendor/fluent-ffmpeg ``` ## Usage You will find a lot of usage examples (including a real-time streaming example using [flowplayer](http://www.flowplayer.org) and [express](https://github.com/visionmedia/express)!) in the `examples` folder. ### Prerequisites #### ffmpeg and ffprobe 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:mc3man/trusty-media` PPA provides recent builds). #### flvtool2 or flvmeta 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. If you set either the `FLVTOOL2_PATH` or `FLVMETA_PATH`, fluent-ffmpeg will try to use it instead of searching in the `PATH`. #### Setting binary paths manually Alternatively, you may set the ffmpeg, ffprobe and flvtool2/flvmeta binary paths manually by using the following API commands: * **Ffmpeg.setFfmpegPath(path)** Argument `path` is a string with the full path to the ffmpeg binary. * **Ffmpeg.setFfprobePath(path)** Argument `path` is a string with the full path to the ffprobe binary. * **Ffmpeg.setFlvtoolPath(path)** Argument `path` is a string with the full path to the flvtool2 or flvmeta binary. ### Creating an FFmpeg command The fluent-ffmpeg module returns a constructor that you can use to instanciate FFmpeg commands. ```js var FfmpegCommand = require('fluent-ffmpeg'); var command = new FfmpegCommand(); ``` You can also use the constructor without the `new` operator. ```js var ffmpeg = require('fluent-ffmpeg'); var command = ffmpeg(); ``` You may pass an input file name or readable stream, a configuration object, or both to the constructor. ```js var command = ffmpeg('/path/to/file.avi'); var command = ffmpeg(fs.createReadStream('/path/to/file.avi')); var command = ffmpeg({ option: "value", ... }); var command = ffmpeg('/path/to/file.avi', { option: "value", ... }); ``` The following options are available: * `source`: input file name or readable stream (ignored if an input file is passed to the constructor) * `timeout`: ffmpeg timeout in seconds (defaults to no timeout) * `preset` or `presets`: directory to load module presets from (defaults to the `lib/presets` directory in fluent-ffmpeg tree) * `niceness` or `priority`: ffmpeg niceness value, between -20 and 20; ignored on Windows platforms (defaults to 0) * `logger`: logger object with `debug()`, `info()`, `warn()` and `error()` methods (defaults to no logging) * `stdoutLines`: maximum number of lines from ffmpeg stdout/stderr to keep in memory (defaults to 100, use 0 for unlimited storage) ### Specifying inputs You can add any number of inputs to an Ffmpeg command. An input can be: * a file name (eg. `/path/to/file.avi`); * an image pattern (eg. `/path/to/frame%03d.png`); * a readable stream; only one input stream may be used for a command, but you can use both an input stream and one or several file names. ```js // Note that all fluent-ffmpeg methods are chainable ffmpeg('/path/to/input1.avi') .input('/path/to/input2.avi') .input(fs.createReadStream('/path/to/input3.avi')); // Passing an input to the constructor is the same as calling .input() ffmpeg() .input('/path/to/input1.avi') .input('/path/to/input2.avi'); // Most methods have several aliases, here you may use addInput or mergeAdd instead ffmpeg() .addInput('/path/to/frame%02d.png') .addInput('/path/to/soundtrack.mp3'); ffmpeg() .mergeAdd('/path/to/input1.avi') .mergeAdd('/path/to/input2.avi'); ``` ### Input options The following methods enable passing input-related options to ffmpeg. Each of these methods apply on the last input added (including the one passed to the constructor, if any). You must add an input before calling those, or an error will be thrown. #### inputFormat(format): specify input format **Aliases**: `fromFormat()`, `withInputFormat()`. This is only useful for raw inputs, as ffmpeg can determine the input format automatically. ```js ffmpeg() .input('/dev/video0') .inputFormat('mov') .input('/path/to/file.avi') .inputFormat('avi'); ``` Fluent-ffmpeg checks for format availability before actually running the command, and throws an error when a specified input format is not available. #### inputFPS(fps): specify input framerate **Aliases**: `withInputFps()`, `withInputFPS()`, `withFpsInput()`, `withFPSInput()`, `inputFps()`, `fpsInput()`, `FPSInput()`. This is only valid for raw inputs, as ffmpeg can determine the input framerate automatically. ```js ffmpeg('/dev/video0').inputFPS(29.7); ``` #### native(): read input at native framerate **Aliases**: `nativeFramerate()`, `withNativeFramerate()`. ```js ffmpeg('/path/to/file.avi').native(); ``` #### seekInput(time): set input start time **Alias**: `setStartTime()`. Seeks an input and only start decoding at given time offset. The `time` argument may be a number (in seconds) or a timestamp string (with format `[[hh:]mm:]ss[.xxx]`). ```js ffmpeg('/path/to/file.avi').seekInput(134.5); ffmpeg('/path/to/file.avi').seekInput('2:14.500'); ``` #### loop([duration]): loop over input ```js ffmpeg('/path/to/file.avi').loop(); ffmpeg('/path/to/file.avi').loop(134.5); ffmpeg('/path/to/file.avi').loop('2:14.500'); ``` #### inputOptions(option...): add custom input options **Aliases**: `inputOption()`, `addInputOption()`, `addInputOptions()`, `withInputOption()`, `withInputOptions()`. This method allows passing any input-related option to ffmpeg. You can call it with a single argument to pass a single option, optionally with a space-separated parameter: ```js /* Single option */ ffmpeg('/path/to/file.avi').inputOptions('-someOption'); /* Single option with parameter */ ffmpeg('/dev/video0').inputOptions('-r 24'); ``` You may also pass multiple options at once by passing an array to the method: ```js ffmpeg('/path/to/file.avi').inputOptions([ '-option1', '-option2 param2', '-option3', '-option4 param4' ]); ``` Finally, you may also directly pass command line tokens as separate arguments to the method: ```js ffmpeg('/path/to/file.avi').inputOptions( '-option1', '-option2', 'param2', '-option3', '-option4', 'param4' ); ``` ### Audio options The following methods change the audio stream(s) in the produced output. #### noAudio(): disable audio altogether **Aliases**: `withNoAudio()`. Disables audio in the output and remove any previously set audio option. ```js ffmpeg('/path/to/file.avi').noAudio(); ``` #### audioCodec(codec): set audio codec **Aliases**: `withAudioCodec()`. ```js ffmpeg('/path/to/file.avi').audioCodec('libmp3lame'); ``` Fluent-ffmpeg checks for codec availability before actually running the command, and throws an error when a specified audio codec is not available. #### audioBitrate(bitrate): set audio bitrate **Aliases**: `withAudioBitrate()`. Sets the audio bitrate in kbps. The `bitrate` parameter may be a number or a string with an optional `k` suffix. This method is used to enforce a constant bitrate; use `audioQuality()` to encode using a variable bitrate. ```js ffmpeg('/path/to/file.avi').audioBitrate(128); ffmpeg('/path/to/file.avi').audioBitrate('128'); ffmpeg('/path/to/file.avi').audioBitrate('128k'); ``` #### audioChannels(count): set audio channel count **Aliases**: `withAudioChannels()`. ```js ffmpeg('/path/to/file.avi').audioChannels(2); ``` #### audioFrequency(freq): set audio frequency **Aliases**: `withAudioFrequency()`. The `freq` parameter specifies the audio frequency in Hz. ```js ffmpeg('/path/to/file.avi').audioFrequency(22050); ``` #### audioQuality(quality): set audio quality **Aliases**: `withAudioQuality()`. This method fixes a quality factor for the audio codec (VBR encoding). The quality scale depends on the actual codec used. ```js ffmpeg('/path/to/file.avi') .audioCodec('libmp3lame') .audioQuality(0); ``` #### audioFilters(filter...): add custom audio filters **Aliases**: `audioFilter()`, `withAudioFilter()`, `withAudioFilters()`. This method enables adding custom audio filters. You may add multiple filters at once by passing either several arguments or an array. See the Ffmpeg documentation for available filters and their syntax. Each filter pased to this method can be either a filter string (eg. `volume=0.5`) or a filter specification object with the following keys: * `filter`: filter name * `options`: optional; either an option string for the filter (eg. `n=-50dB:d=5`), an options array for unnamed options (eg. `['-50dB', 5]`) or an object mapping option names to values (eg. `{ n: '-50dB', d: 5 }`). When `options` is not specified, the filter will be added without any options. ```js ffmpeg('/path/to/file.avi') .audioFilters('volume=0.5') .audioFilters('silencedetect=n=-50dB:d=5'); ffmpeg('/path/to/file.avi') .audioFilters('volume=0.5', 'silencedetect=n=-50dB:d=5'); ffmpeg('/path/to/file.avi') .audioFilters(['volume=0.5', 'silencedetect=n=-50dB:d=5']); ffmpeg('/path/to/file.avi') .audioFilters([ { filter: 'volume', options: '0.5' }, { filter: 'silencedetect', options: 'n=-50dB:d=5' } ]); ffmpeg('/path/to/file.avi') .audioFilters( { filter: 'volume', options: ['0.5'] }, { filter: 'silencedetect', options: { n: '-50dB', d: 5 } } ]); ``` ### Video options The following methods change the video stream(s) in the produced output. #### noVideo(): disable video altogether **Aliases**: `withNoVideo()`. This method disables video output and removes any previously set video option. ```js ffmpeg('/path/to/file.avi').noVideo(); ``` #### videoCodec(codec): set video codec **Aliases**: `withVideoCodec()`. ```js ffmpeg('/path/to/file.avi').videoCodec('libx264'); ``` Fluent-ffmpeg checks for codec availability before actually running the command, and throws an error when a specified video codec is not available. #### videoBitrate(bitrate[, constant=false]): set video bitrate **Aliases**: `withVideoBitrate()`. Sets the target video bitrate in kbps. The `bitrate` argument may be a number or a string with an optional `k` suffix. The `constant` argument specifies whether a constant bitrate should be enforced (defaults to false). Keep in mind that, depending on the codec used, enforcing a constant bitrate often comes at the cost of quality. The best way to have a constant video bitrate without losing too much quality is to use 2-pass encoding (see Fffmpeg documentation). ```js ffmpeg('/path/to/file.avi').videoBitrate(1000); ffmpeg('/path/to/file.avi').videoBitrate('1000'); ffmpeg('/path/to/file.avi').videoBitrate('1000k'); ffmpeg('/path/to/file.avi').videoBitrate('1000k', true); ``` #### videoFilters(filter...): add custom video filters **Aliases**: `videoFilter()`, `withVideoFilter()`, `withVideoFilters()`. This method enables adding custom video filters. You may add multiple filters at once by passing either several arguments or an array. See the Ffmpeg documentation for available filters and their syntax. Each filter pased to this method can be either a filter string (eg. `fade=in:0:30`) or a filter specification object with the following keys: * `filter`: filter name * `options`: optional; either an option string for the filter (eg. `in:0:30`), an options array for unnamed options (eg. `['in', 0, 30]`) or an object mapping option names to values (eg. `{ t: 'in', s: 0, n: 30 }`). When `options` is not specified, the filter will be added without any options. ```js ffmpeg('/path/to/file.avi') .videoFilters('fade=in:0:30') .videoFilters('pad=640:480:0:40:violet'); ffmpeg('/path/to/file.avi') .videoFilters('fade=in:0:30', 'pad=640:480:0:40:violet'); ffmpeg('/path/to/file.avi') .videoFilters(['fade=in:0:30', 'pad=640:480:0:40:violet']); ffmpeg('/path/to/file.avi') .videoFilters([ { filter: 'fade', options: 'in:0:30' }, { filter: 'pad', options: '640:480:0:40:violet' } ]); ffmpeg('/path/to/file.avi') .videoFilters( { filter: 'fade', options: ['in', 0, 30] }, { filter: 'filter2', options: { w: 640, h: 480, x: 0, y: 40, color: 'violet' } } ); ``` #### fps(fps): set output framerate **Aliases**: `withOutputFps()`, `withOutputFPS()`, `withFpsOutput()`, `withFPSOutput()`, `withFps()`, `withFPS()`, `outputFPS()`, `outputFps()`, `fpsOutput()`, `FPSOutput()`, `FPS()`. ```js ffmpeg('/path/to/file.avi').fps(29.7); ``` #### frames(count): specify frame count **Aliases**: `takeFrames()`, `withFrames()`. Set ffmpeg to only encode a certain number of frames. ```js ffmpeg('/path/to/file.avi').frames(240); ``` ### Video frame size options The following methods enable resizing the output video frame size. They all work together to generate the appropriate video filters. #### size(size): set output frame size **Aliases**: `videoSize()`, `withSize()`. This method sets the output frame size. The `size` argument may have one of the following formats: * `640x480`: set a fixed output frame size. Unless `autopad()` is called, this may result in the video being stretched or squeezed to fit the requested size. * `640x?`: set a fixed width and compute height automatically. If `aspect()` is also called, it is used to compute video height; otherwise it is computed so that the input aspect ratio is preserved. * `?x480`: set a fixed height and compute width automatically. If `aspect()` is also called, it is used to compute video width; otherwise it is computed so that the input aspect ratio is preserved. * `50%`: rescale both width and height to the given percentage. Aspect ratio is always preserved. Note that for compatibility with some codecs, computed dimensions are always rounded down to multiples of 2. ```js ffmpeg('/path/to/file.avi').size('640x480'); ffmpeg('/path/to/file.avi').size('640x?'); ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3'); ffmpeg('/path/to/file.avi').size('50%'); ``` #### aspect(aspect): set output frame aspect ratio **Aliases**: `withAspect()`, `withAspectRatio()`, `setAspect()`, `setAspectRatio()`, `aspectRatio()`. This method enforces a specific output aspect ratio. The `aspect` argument may either be a number or a `X:Y` string. Note that calls to `aspect()` are ignored when `size()` has been called with a fixed width and height or a percentage, and also when `size()` has not been called at all. ```js ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3'); ffmpeg('/path/to/file.avi').size('640x?').aspect(1.33333); ``` #### autopad([color='black']): enable auto-padding the output video **Aliases**: `applyAutopadding()`, `applyAutoPadding()`, `applyAutopad()`, `applyAutoPad()`, `withAutopadding()`, `withAutoPadding()`, `withAutopad()`, `withAutoPad()`, `autoPad()`. This method enables applying auto-padding to the output video. The `color` parameter specifies which color to use for padding, and must be a color code or name supported by ffmpeg (defaults to 'black'). The behaviour of this method depends on calls made to other video size methods: * when `size()` has been called with a percentage or has not been called, it is ignored; * when `size()` has been called with `WxH`, it adds padding so that the input aspect ratio is kept; * when `size()` has been called with either `Wx?` or `?xH`, padding is only added if `aspect()` was called (otherwise the output dimensions are computed from the input aspect ratio and padding is not needed). ```js // No size specified, autopad() is ignored ffmpeg('/path/to/file.avi').autopad(); // Adds padding to keep original aspect ratio. // - with a 640x400 input, 40 pixels of padding are added on both sides // - with a 600x480 input, 20 pixels of padding are added on top and bottom // - with a 320x200 input, video is scaled up to 640x400 and 40px of padding // is added on both sides // - with a 320x240 input, video is scaled up to 640x480 and and no padding // is needed ffmpeg('/path/to/file.avi').size('640x480').autopad(); ffmpeg('/path/to/file.avi').size('640x480').autopad('white'); ffmpeg('/path/to/file.avi').size('640x480').autopad('#35A5FF'); // Size computed from input, autopad() is ignored ffmpeg('/path/to/file.avi').size('50%').autopad(); ffmpeg('/path/to/file.avi').size('640x?').autopad(); ffmpeg('/path/to/file.avi').size('?x480').autopad(); // Calling .size('640x?').aspect('4:3') is similar to calling .size('640x480') // - with a 640x400 input, 40 pixels of padding are added on both sides // - with a 600x480 input, 20 pixels of padding are added on top and bottom // - with a 320x200 input, video is scaled up to 640x400 and 40px of padding // is added on both sides // - with a 320x240 input, video is scaled up to 640x480 and and no padding // is needed ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad(); ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad('white'); ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad('#35A5FF'); // Calling .size('?x480').aspect('4:3') is similar to calling .size('640x480') ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad(); ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad('white'); ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad('#35A5FF'); ``` For compatibility with previous fluent-ffmpeg versions, this method also accepts an additional boolean first argument, which specifies whether to apply auto-padding. ```js ffmpeg('/path/to/file.avi').size('640x480').autopad(true); ffmpeg('/path/to/file.avi').size('640x480').autopad(true, 'pink'); ``` #### keepDAR(): force keeping display aspect ratio **Aliases**: `keepPixelAspect()`, `keepDisplayAspect()`, `keepDisplayAspectRatio()`. This method is useful when converting an input with non-square pixels to an output format that does not support non-square pixels (eg. most image formats). It rescales the input so that the display aspect ratio is the same. ```js ffmpeg('/path/to/file.avi').keepDAR(); ``` ### Specifying multiple outputs #### output(target[, options]): add an output to the command **Aliases**: `addOutput()`. Adds an output to the command. The `target` argument may be an output filename or a writable stream (but at most one output stream may be used with a single command). When `target` is a stream, an additional `options` object may be passed. If it is present, it will be passed ffmpeg output stream `pipe()` method. Adding an output switches the "current output" of the command, so that any fluent-ffmpeg method that applies to an output is indeed applied to the last output added. For backwards compatibility reasons, you may as well call those methods _before_ adding the first output (in which case they will apply to the first output when it is added). Methods that apply to an output are all non-input-related methods, except for `complexFilter()`, which is global. Also note that when calling `output()`, you should not use the `save()` or `stream()` (formerly `saveToFile()` and `writeToStream()`) methods, as they already add an output. Use the `run()` method to start processing. ```js var stream = fs.createWriteStream('outputfile.divx'); ffmpeg('/path/to/file.avi') .output('outputfile.mp4') .output(stream); ffmpeg('/path/to/file.avi') // You may pass a pipe() options object when using a stream .output(stream, { end:true }); // Output-related methods apply to the last output added ffmpeg('/path/to/file.avi') .output('outputfile.mp4') .audioCodec('libfaac') .videoCodec('libx264') .size('320x200') .output(stream) .preset('divx') .size('640x480'); // Use the run() method to run commands with multiple outputs ffmpeg('/path/to/file.avi') .output('outputfile.mp4') .output(stream) .on('end', function() { console.log('Finished processing'); }) .run(); ``` ### Output options #### duration(time): set output duration **Aliases**: `withDuration()`, `setDuration()`. Forces ffmpeg to stop transcoding after a specific output duration. The `time` parameter may be a number (in seconds) or a timestamp string (with format `[[hh:]mm:]ss[.xxx]`). ```js ffmpeg('/path/to/file.avi').duration(134.5); ffmpeg('/path/to/file.avi').duration('2:14.500'); ``` #### seek(time): seek output **Aliases**: `seekOutput()`. Seeks streams before encoding them into the output. This is different from calling `seekInput()` in that the offset will only apply to one output. This is also slower, as skipped frames will still be decoded (but dropped). The `time` argument may be a number (in seconds) or a timestamp string (with format `[[hh:]mm:]ss[.xxx]`). ```js ffmpeg('/path/to/file.avi') .seekInput('1:00') .output('from-1m30s.avi') .seek(30) .output('from-1m40s.avi') .seek('0:40'); ``` #### format(format): set output format **Aliases**: `withOutputFormat()`, `toFormat()`, `outputFormat()`. ```js ffmpeg('/path/to/file.avi').format('flv'); ``` #### flvmeta(): update FLV metadata after transcoding **Aliases**: `updateFlvMetadata()`. Calling this method makes fluent-ffmpeg run `flvmeta` or `flvtool2` on the output file to add FLV metadata and make files streamable. It does not work when outputting to a stream, and is only useful when outputting to FLV format. ```js ffmpeg('/path/to/file.avi').flvmeta().format('flv'); ``` #### outputOptions(option...): add custom output options **Aliases**: `outputOption()`, `addOutputOption()`, `addOutputOptions()`, `withOutputOption()`, `withOutputOptions()`, `addOption()`, `addOptions()`. This method allows passing any output-related option to ffmpeg. You can call it with a single argument to pass a single option, optionally with a space-separated parameter: ```js /* Single option */ ffmpeg('/path/to/file.avi').outputOptions('-someOption'); /* Single option with parameter */ ffmpeg('/dev/video0').outputOptions('-r 24'); ``` You may also pass multiple options at once by passing an array to the method: ```js ffmpeg('/path/to/file.avi').outputOptions([ '-option1', '-option2 param2', '-option3', '-option4 param4' ]); ``` Finally, you may also directly pass command line tokens as separate arguments to the method: ```js ffmpeg('/path/to/file.avi').outputOptions( '-option1', '-option2', 'param2', '-option3', '-option4', 'param4' ); ``` ### Miscellaneous options #### preset(preset): use fluent-ffmpeg preset **Aliases**: `usingPreset()`. There are two kinds of presets supported by fluent-ffmpeg. The first one is preset modules; to use those, pass the preset name as the `preset` argument. Preset modules are loaded from the directory specified by the `presets` constructor option (defaults to the `lib/presets` fluent-ffmpeg subdirectory). ```js // Uses /lib/presets/divx.js ffmpeg('/path/to/file.avi').preset('divx'); // Uses /my/presets/foo.js ffmpeg('/path/to/file.avi', { presets: '/my/presets' }).preset('foo'); ``` Preset modules must export a `load()` function that takes an FfmpegCommand as an argument. fluent-ffmpeg comes with the following preset modules preinstalled: * `divx` * `flashvideo` * `podcast` Here is the code from the included `divx` preset as an example: ```js exports.load = function(ffmpeg) { ffmpeg .format('avi') .videoBitrate('1024k') .videoCodec('mpeg4') .size('720x?') .audioBitrate('128k') .audioChannels(2) .audioCodec('libmp3lame') .outputOptions(['-vtag DIVX']); }; ``` The second kind of preset is preset functions. To use those, pass a function which takes an FfmpegCommand as a parameter. ```js function myPreset(command) { command.format('avi').size('720x?'); } ffmpeg('/path/to/file.avi').preset(myPreset); ``` #### complexFilter(filters[, map]): set complex filtergraph **Aliases**: `filterGraph()` The `complexFilter()` method enables setting a complex filtergraph for a command. It expects a filter specification (or a filter specification array) and an optional output mapping parameter as arguments. Filter specifications may be either plain ffmpeg filter strings (eg. `split=3[a][b][c]`) or objects with the following keys: * `filter`: filter name * `options`: optional; either an option string for the filter (eg. `in:0:30`), an options array for unnamed options (eg. `['in', 0, 30]`) or an object mapping option names to values (eg. `{ t: 'in', s: 0, n: 30 }`). When `options` is not specified, the filter will be added without any options. * `inputs`: optional; input stream specifier(s) for the filter. The value may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets. When input streams are not specified, ffmpeg will use the first unused streams of the correct type. * `outputs`: optional; output stream specifier(s) for the filter. The value may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets. The output mapping parameter specifies which stream(s) to include in the output from the filtergraph. It may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets. When this parameter is not present, ffmpeg will default to saving all unused outputs to the output file. Note that only one complex filtergraph may be set on a given command. Calling `complexFilter()` again will override any previously set filtergraph, but you can set as many filters as needed in a single call. ```js ffmpeg('/path/to/file.avi') .complexFilter([ // Rescale input stream into stream 'rescaled' 'scale=640:480[rescaled]', // Duplicate rescaled stream 3 times into streams a, b, and c { filter: 'split', options: '3', inputs: 'rescaled', outputs: ['a', 'b', 'c'] }, // Create stream 'red' by removing green and blue channels from stream 'a' { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' }, // Create stream 'green' by removing red and blue channels from stream 'b' { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' }, // Create stream 'blue' by removing red and green channels from stream 'c' { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' }, // Pad stream 'red' to 3x width, keeping the video on the left, // and name output 'padded' { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' }, // Overlay 'green' onto 'padded', moving it to the center, // and name output 'redgreen' { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen' }, // Overlay 'blue' onto 'redgreen', moving it to the right { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue'], outputs: 'output' }, ], 'output'); ``` ### Setting event handlers Before actually running a command, you may want to set event listeners on it to be notified when it's done. The following events are available: #### 'start': ffmpeg process started The `start` event is emitted just after ffmpeg has been spawned. It is emitted with the full command line used as an argument. ```js ffmpeg('/path/to/file.avi') .on('start', function(commandLine) { console.log('Spawned Ffmpeg with command: ' + commandLine); }); ``` #### 'codecData': input codec data available The `codecData` event is emitted when ffmpeg outputs codec information about its input streams. It is emitted with an object argument with the following keys: * `format`: input format * `duration`: input duration * `audio`: audio codec * `audio_details`: audio encoding details * `video`: video codec * `video_details`: video encoding details ```js ffmpeg('/path/to/file.avi') .on('codecData', function(data) { console.log('Input is ' + data.audio + ' audio ' + 'with ' + data.video + ' video'); }); ``` #### 'progress': transcoding progress information The `progress` event is emitted every time ffmpeg reports progress information. It is emitted with an object argument with the following keys: * `frames`: total processed frame count * `currentFps`: framerate at which FFmpeg is currently processing * `currentKbps`: throughput at which FFmpeg is currently processing * `targetSize`: current size of the target file in kilobytes * `timemark`: the timestamp of the current frame in seconds * `percent`: an estimation of the progress percentage Note that `percent` can be (very) inaccurate, as the only progress information fluent-ffmpeg gets from ffmpeg is the total number of frames written (and the corresponding duration). To estimate percentage, fluent-ffmpeg has to guess what the total output duration will be, and uses the first input added to the command to do so. In particular: * percentage is not available when using an input stream * percentage may be wrong when using multiple inputs with different durations and the first one is not the longest ```js ffmpeg('/path/to/file.avi') .on('progress', function(progress) { console.log('Processing: ' + progress.percent + '% done'); }); ``` #### 'stderr': FFmpeg output The `stderr` event is emitted every time FFmpeg outputs a line to `stderr`. It is emitted with a string containing the line of stderr (minus trailing new line characters). ```js ffmpeg('/path/to/file.avi') .on('stderr', function(stderrLine) { console.log('Stderr output: ' + stderrLine); }); ``` #### 'error': transcoding error The `error` event is emitted when an error occurs when running ffmpeg or when preparing its execution. It is emitted with an error object as an argument. If the error happened during ffmpeg execution, listeners will also receive two additional arguments containing ffmpegs stdout and stderr. If streams are used for input or output, any errors emitted from these streams will be passed through to this event, attached to the `error` as `inputStreamError` and `outputStreamError` for input and output streams respectively. **Warning**: you should _always_ set a handler for the `error` event, as node's default behaviour when an `error` event without any listeners is emitted is to output the error to the console and _terminate the program_. ```js ffmpeg('/path/to/file.avi') .on('error', function(err, stdout, stderr) { console.log('Cannot process video: ' + err.message); }); ``` #### 'end': processing finished The `end` event is emitted when processing has finished. Listeners receive ffmpeg standard output and standard error as arguments, except when generating thumbnails (see below), in which case they receive an array of the generated filenames. ```js ffmpeg('/path/to/file.avi') .on('end', function(stdout, stderr) { console.log('Transcoding succeeded !'); }); ``` `stdout` is empty when the command outputs to a stream. Both `stdout` and `stderr` are limited by the `stdoutLines` option (defaults to 100 lines). ### Starting FFmpeg processing #### save(filename): save the output to a file **Aliases**: `saveToFile()` Starts ffmpeg processing and saves the output to a file. ```js ffmpeg('/path/to/file.avi') .videoCodec('libx264') .audioCodec('libmp3lame') .size('320x240') .on('error', function(err) { console.log('An error occurred: ' + err.message); }) .on('end', function() { console.log('Processing finished !'); }) .save('/path/to/output.mp4'); ``` Note: the `save()` method is actually syntactic sugar for calling both `output()` and `run()`. #### pipe([stream], [options]): pipe the output to a writable stream **Aliases**: `stream()`, `writeToStream()`. Starts processing and pipes ffmpeg output to a writable stream. The `options` argument, if present, is passed to ffmpeg output stream's `pipe()` method (see nodejs documentation). ```js var outStream = fs.createWriteStream('/path/to/output.mp4'); ffmpeg('/path/to/file.avi') .videoCodec('libx264') .audioCodec('libmp3lame') .size('320x240') .on('error', function(err) { console.log('An error occurred: ' + err.message); }) .on('end', function() { console.log('Processing finished !'); }) .pipe(outStream, { end: true }); ``` When no `stream` argument is present, the `pipe()` method returns a PassThrough stream, which you can pipe to somewhere else (or just listen to events on). **Note**: this is only available with node >= 0.10. ```js var command = ffmpeg('/path/to/file.avi') .videoCodec('libx264') .audioCodec('libmp3lame') .size('320x240') .on('error', function(err) { console.log('An error occurred: ' + err.message); }) .on('end', function() { console.log('Processing finished !'); }); var ffstream = command.pipe(); ffstream.on('data', function(chunk) { console.log('ffmpeg just wrote ' + chunk.length + ' bytes'); }); ``` Note: the `stream()` method is actually syntactic sugar for calling both `output()` and `run()`. #### run(): start processing **Aliases**: `exec()`, `execute()`. This method is mainly useful when producing multiple outputs (otherwise the `save()` or `stream()` methods are more straightforward). It starts processing with the specified outputs. **Warning**: do not use `run()` when calling other processing methods (eg. `save()`, `pipe()` or `screenshots()`). ```js ffmpeg('/path/to/file.avi') .output('screenshot.png') .noAudio() .seek('3:00') .output('small.avi') .audioCodec('copy') .size('320x200') .output('big.avi') .audioCodec('copy') .size('640x480') .on('error', function(err) { console.log('An error occurred: ' + err.message); }) .on('end', function() { console.log('Processing finished !'); }) .run(); ``` #### mergeToFile(filename, tmpdir): concatenate multiple inputs Use the `input` 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. ```js ffmpeg('/path/to/part1.avi') .input('/path/to/part2.avi') .input('/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'); ``` #### screenshots(options[, dirname]): generate thumbnails **Aliases**: `thumbnail()`, `thumbnails()`, `screenshot()`, `takeScreenshots()`. Use the `screenshots` method to extract one or several thumbnails and save them as PNG files. There are a few caveats with this implementation, though: * It will not work on input streams. * Progress information reported by the `progress` event is not accurate. * It doesn't interract well with filters. In particular, don't use the `size()` method to resize thumbnails, use the `size` option instead. The `options` argument is an object with the following keys: * `folder`: output folder for generated image files. Defaults to the current folder. * `filename`: output filename pattern (see below). Defaults to "tn.png". * `count`: specifies how many thumbnails to generate. When using this option, thumbnails are generated at regular intervals in the video (for example, when requesting 3 thumbnails, at 25%, 50% and 75% of the video length). `count` is ignored when `timemarks` or `timestamps` is specified. * `timemarks` or `timestamps`: specifies an array of timestamps in the video where thumbnails should be taken. Each timestamp may be a number (in seconds), a percentage string (eg. "50%") or a timestamp string with format "hh:mm:ss.xxx" (where hours, minutes and milliseconds are both optional). * `size`: specifies a target size for thumbnails (with the same format as the `.size()` method). **Note:** you should not use the `.size()` method when generating thumbnails. The `filename` option specifies a filename pattern for generated files. It may contain the following format tokens: * '%s': offset in seconds * '%w': screenshot width * '%h': screenshot height * '%r': screenshot resolution (same as '%wx%h') * '%f': input filename * '%b': input basename (filename w/o extension) * '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`) If multiple timemarks are passed and no variable format token ('%s' or '%i') is specified in the filename pattern, `_%i` will be added automatically. When generating thumbnails, an additional `filenames` event is dispatched with an array of generated filenames as an argument. ```js ffmpeg('/path/to/video.avi') .on('filenames', function(filenames) { console.log('Will generate ' + filenames.join(', ')) }) .on('end', function() { console.log('Screenshots taken'); }) .screenshots({ // Will take screens at 20%, 40%, 60% and 80% of the video count: 4, folder: '/path/to/output' }); ffmpeg('/path/to/video.avi') .screenshots({ timestamps: [30.5, '50%', '01:10.123'], filename: 'thumbnail-at-%s-seconds.png', folder: '/path/to/output', size: '320x240' }); ``` ### Controlling the FFmpeg process #### kill([signal='SIGKILL']): kill any running ffmpeg process This method sends `signal` (defaults to 'SIGKILL') to the ffmpeg process. It only has sense when processing has started. Sending a signal that terminates the process will result in the `error` event being emitted. ```js var command = ffmpeg('/path/to/video.avi') .videoCodec('libx264') .audioCodec('libmp3lame') .on('start', function() { // Send SIGSTOP to suspend ffmpeg command.kill('SIGSTOP'); doSomething(function() { // Send SIGCONT to resume ffmpeg command.kill('SIGCONT'); }); }) .save('/path/to/output.mp4'); // Kill ffmpeg after 60 seconds anyway setTimeout(function() { command.on('error', function() { console.log('Ffmpeg has been killed'); }); command.kill(); }, 60000); ``` #### renice([niceness=0]): change ffmpeg process priority This method alters the niceness (priority) value of any running ffmpeg process (if any) and any process spawned in the future. The `niceness` parameter may range from -20 (highest priority) to 20 (lowest priority) and defaults to 0 (which is the default process niceness on most *nix systems). **Note**: this method is ineffective on Windows platforms. ```js // Set startup niceness var command = ffmpeg('/path/to/file.avi') .renice(5) .save('/path/to/output.mp4'); // Command takes too long, raise its priority setTimeout(function() { command.renice(-5); }, 60000); ``` ### Reading video metadata You can read metadata from any valid ffmpeg input file with the modules `ffprobe` method. ```js ffmpeg.ffprobe('/path/to/file.avi', function(err, metadata) { console.dir(metadata); }); ``` You may also call the ffprobe method on an FfmpegCommand to probe one of its input. You may pass a 0-based input number as a first argument to specify which input to read metadata from, otherwise the method will probe the last added input. ```js ffmpeg('/path/to/file1.avi') .input('/path/to/file2.avi') .ffprobe(function(err, data) { console.log('file2 metadata:'); console.dir(data); }); ffmpeg('/path/to/file1.avi') .input('/path/to/file2.avi') .ffprobe(0, function(err, data) { console.log('file1 metadata:'); console.dir(data); }); ``` **Warning:** ffprobe may be called with an input stream, but in this case *it will consume data from the stream*, and this data will no longer be available for ffmpeg. Using both ffprobe and a transcoding command on the same input stream will most likely fail unless the stream is a live stream. Only do this if you know what you're doing. The returned object is the same that is returned by running the following command from your shell (depending on your ffmpeg version you may have to replace `-of` with `-print_format`) : ```sh $ 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: ```js { "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" } } } ``` ### Querying ffmpeg capabilities fluent-ffmpeg enables you to query your installed ffmpeg version for supported formats, codecs, encoders and filters. ```js 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.getAvailableEncoders(function(err, encoders) { console.log('Available encoders:'); console.dir(encoders); }); 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: ```js { ... mp4: { description: 'MP4 (MPEG-4 Part 14)', canDemux: false, canMux: true }, ... } ``` * `canDemux` indicates whether ffmpeg is able to extract streams from (demux) this format * `canMux` indicates whether ffmpeg is able to write streams into (mux) this format The returned object for codecs looks like: ```js { ... 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 codec * `canEncode` 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 purposes * `intraFrameOnly` tells if codec can only work with I-frames * `isLossy` tells if codec can do lossy encoding/decoding * `isLossless` 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 encoders looks like: ```js { ... libmp3lame: { type: 'audio', description: 'MP3 (MPEG audio layer 3) (codec mp3)', frameMT: false, sliceMT: false, experimental: false, drawHorizBand: false, directRendering: false }, ... } ``` * `type` indicates the encoder type, either "audio", "video" or "subtitle" * `experimental` indicates whether the encoder is experimental. When using such a codec, fluent-ffmpeg automatically adds the '-strict experimental' flag. The returned object for filters looks like: ```js { ... 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 nothing * `multipleInputs` tells whether the filter can accept multiple inputs * `output` 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 ### Cloning an FfmpegCommand You can create clones of an FfmpegCommand instance by calling the `clone()` method. The clone will be an exact copy of the original at the time it has been called (same inputs, same options, same event handlers, etc.). This is mainly useful when you want to apply different processing options on the same input. Setting options, adding inputs or event handlers on a clone will not affect the original command. ```js // Create a command to convert source.avi to MP4 var command = ffmpeg('/path/to/source.avi') .audioCodec('libfaac') .videoCodec('libx264') .format('mp4'); // Create a clone to save a small resized version command.clone() .size('320x200') .save('/path/to/output-small.mp4'); // Create a clone to save a medium resized version command.clone() .size('640x400') .save('/path/to/output-medium.mp4'); // Save a converted version with the original size command.save('/path/to/output-original-size.mp4'); ``` ## Contributing 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! ### Code contributions If you want to add new features or change the API, please submit an issue first to make sure no one else is already working on the same thing and discuss the implementation and API details with maintainers and users by creating an issue. When everything is settled down, you can submit a pull request. When fixing bugs, you can directly submit a pull request. Make sure to add tests for your features and bugfixes and update the documentation (see below) before submitting your code! ### Documentation contributions You can directly submit pull requests for documentation changes. Make sure to regenerate the documentation before submitting (see below). ### Updating the documentation When contributing API changes (new methods for example), be sure to update the README file and JSDoc comments in the code. fluent-ffmpeg comes with a plugin that enables two additional JSDoc tags: * `@aliases`: document method aliases ```js /** * ... * @method FfmpegCommand#myMethod * @aliases myMethodAlias,myOtherMethodAlias */ ``` * `@category`: set method category ```js /** * ... * @category Audio */ ``` You can regenerate the JSDoc documentation by running the following command: ```sh $ make doc ``` To avoid polluting the commit history, make sure to only commit the regenerated JSDoc once and in a specific commit. ### Running tests To run unit tests, first make sure you installed npm dependencies (run `npm install`). ```sh $ make test ``` Make sure your ffmpeg installation is up-to-date to prevent strange assertion errors because of missing codecs/bugfixes. ## Main contributors * [enobrev](http://github.com/enobrev) * [njoyard](http://github.com/njoyard) * [sadikzzz](http://github.com/sadikzzz) * [smremde](http://github.com/smremde) * [spruce](http://github.com/spruce) * [tagedieb](http://github.com/tagedieb) * [tommadema](http://github.com/tommadema) * [Weltschmerz](http://github.com/Weltschmerz) ## License (The MIT License) Copyright (c) 2011 Stefan Schaermeli <schaermu@gmail.com> 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. [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffluent-ffmpeg%2Fnode-fluent-ffmpeg.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffluent-ffmpeg%2Fnode-fluent-ffmpeg?ref=badge_large) ================================================ FILE: doc/FfmpegCommand.html ================================================ JSDoc: Class: FfmpegCommand

Class: FfmpegCommand

FfmpegCommand

new FfmpegCommand(input, options)

Create an ffmpeg command

Can be called with or without the 'new' operator, and the 'input' parameter may be specified as 'options.source' instead (or passed later with the addInput method).

Parameters:
Name Type Argument Description
input String | ReadableStream <optional>

input file path or readable stream

options Object <optional>

command options

Properties
Name Type Argument Default Description
logger Object <optional>
<no logging>

logger object with 'error', 'warning', 'info' and 'debug' methods

niceness Number <optional>
0

ffmpeg process niceness, ignored on Windows

priority Number <optional>
0

alias for niceness

presets String <optional>
"fluent-ffmpeg/lib/presets"

directory to load presets from

preset String <optional>
"fluent-ffmpeg/lib/presets"

alias for presets

stdoutLines String <optional>
100

maximum lines of ffmpeg output to keep in memory, use 0 for unlimited

timeout Number <optional>
<no timeout>

ffmpeg processing timeout in seconds

source String | ReadableStream <optional>
<no input>

alias for the input parameter

Source:

Audio methods

audioBitrate(bitrate)

Specify audio bitrate

Parameters:
Name Type Description
bitrate String | Number

audio bitrate in kbps (with an optional 'k' suffix)

Source:
Returns:

FfmpegCommand

Alias:
withAudioBitrate

audioChannels(channels)

Specify audio channel count

Parameters:
Name Type Description
channels Number

channel count

Source:
Returns:

FfmpegCommand

Alias:
withAudioChannels

audioCodec(codec)

Specify audio codec

Parameters:
Name Type Description
codec String

audio codec name

Source:
Returns:

FfmpegCommand

Alias:
withAudioCodec

audioFilter(filters)

audioFilters(filters)

Specify custom audio filter(s)

Can be called both with one or many filters, or a filter array.

Parameters:
Name Type Description
filters String | Array.<String> | Array.<Object>

audio filter strings, string array or filter specification array, each with the following properties:

Properties
Name Type Argument Description
filter String

filter name

options String | Array.<String> | Object <optional>

filter option string, array, or object

Source:
Returns:

FfmpegCommand

Examples:
command.audioFilters('filter1');
command.audioFilters('filter1', 'filter2=param1=value1:param2=value2');
command.audioFilters(['filter1', 'filter2']);
command.audioFilters([
  {
    filter: 'filter1'
  },
  {
    filter: 'filter2',
    options: 'param=value:param=value'
  }
]);
command.audioFilters(
  {
    filter: 'filter1',
    options: ['value1', 'value2']
  },
  {
    filter: 'filter2',
    options: { param1: 'value1', param2: 'value2' }
  }
);
Aliases:
  • withAudioFilter
  • withAudioFilters
  • audioFilter

audioFrequency(freq)

Specify audio frequency

Parameters:
Name Type Description
freq Number

audio frequency in Hz

Source:
Returns:

FfmpegCommand

Alias:
withAudioFrequency

audioQuality(quality)

Specify audio quality

Parameters:
Name Type Description
quality Number

audio quality factor

Source:
Returns:

FfmpegCommand

Alias:
withAudioQuality

noAudio()

Disable audio in the output

Source:
Returns:

FfmpegCommand

Alias:
withNoAudio

withAudioBitrate(bitrate)

withAudioChannels(channels)

withAudioCodec(codec)

withAudioFilter(filters)

withAudioFilters(filters)

withAudioFrequency(freq)

withAudioQuality(quality)

withNoAudio()

Capabilities methods

availableCodecs(callback)

Query ffmpeg for available codecs

Parameters:
Name Type Description
callback FfmpegCommand~codecCallback

callback function

Source:
Alias:
getAvailableCodecs

availableEncoders(callback)

Query ffmpeg for available encoders

Parameters:
Name Type Description
callback FfmpegCommand~encodersCallback

callback function

Source:
Alias:
getAvailableEncoders

availableFilters(callback)

Query ffmpeg for available filters

Parameters:
Name Type Description
callback FfmpegCommand~filterCallback

callback function

Source:
Alias:
getAvailableFilters

availableFormats(callback)

Query ffmpeg for available formats

Parameters:
Name Type Description
callback FfmpegCommand~formatCallback

callback function

Source:
Alias:
getAvailableFormats

getAvailableCodecs(callback)

getAvailableEncoders(callback)

getAvailableFilters(callback)

getAvailableFormats(callback)

Custom options methods

addInputOption(options)

addInputOptions(options)

addOption(options)

addOptions(options)

addOutputOption(options)

addOutputOptions(options)

complexFilter(spec, map)

Specify a complex filtergraph

Calling this method will override any previously set filtergraph, but you can set as many filters as needed in one call.

Parameters:
Name Type Argument Description
spec String | Array

filtergraph string or array of filter specification objects, each having the following properties:

Properties
Name Type Argument Description
filter String

filter name

inputs String | Array <optional>

(array of) input stream specifier(s) for the filter, defaults to ffmpeg automatically choosing the first unused matching streams

outputs String | Array <optional>

(array of) output stream specifier(s) for the filter, defaults to ffmpeg automatically assigning the output to the output file

options Object | String | Array <optional>

filter options, can be omitted to not set any options

map Array <optional>

(array of) stream specifier(s) from the graph to include in ffmpeg output, defaults to ffmpeg automatically choosing the first matching streams.

Source:
Returns:

FfmpegCommand

Examples:

Overlay an image over a video (using a filtergraph string)

  ffmpeg()
    .input('video.avi')
    .input('image.png')
    .complexFilter('[0:v][1:v]overlay[out]', ['out']);

Overlay an image over a video (using a filter array)

  ffmpeg()
    .input('video.avi')
    .input('image.png')
    .complexFilter([{
      filter: 'overlay',
      inputs: ['0:v', '1:v'],
      outputs: ['out']
    }], ['out']);

Split video into RGB channels and output a 3x1 video with channels side to side

 ffmpeg()
   .input('video.avi')
   .complexFilter([
     // Duplicate video stream 3 times into streams a, b, and c
     { filter: 'split', options: '3', outputs: ['a', 'b', 'c'] },

     // Create stream 'red' by cancelling green and blue channels from stream 'a'
     { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' },

     // Create stream 'green' by cancelling red and blue channels from stream 'b'
     { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' },

     // Create stream 'blue' by cancelling red and green channels from stream 'c'
     { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' },

     // Pad stream 'red' to 3x width, keeping the video on the left, and name output 'padded'
     { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' },

     // Overlay 'green' onto 'padded', moving it to the center, and name output 'redgreen'
     { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen'},

     // Overlay 'blue' onto 'redgreen', moving it to the right
     { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue']},
   ]);
Alias:
filterGraph

filterGraph(spec, map)

inputOption(options)

inputOptions(options)

Add custom input option(s)

When passing a single string or an array, each string containing two words is split (eg. inputOptions('-option value') is supported) for compatibility reasons. This is not the case when passing more than one argument.

Parameters:
Name Type Argument Description
options String <repeatable>

option string(s) or string array

Source:
Returns:

FfmpegCommand

Examples:
command.inputOptions('option1');
command.inputOptions('option1', 'option2');
command.inputOptions(['option1', 'option2']);
Aliases:
  • addInputOption
  • addInputOptions
  • withInputOption
  • withInputOptions
  • inputOption

outputOption(options)

outputOptions(options)

Add custom output option(s)

Parameters:
Name Type Argument Description
options String <repeatable>

option string(s) or string array

Source:
Returns:

FfmpegCommand

Examples:
command.outputOptions('option1');
command.outputOptions('option1', 'option2');
command.outputOptions(['option1', 'option2']);
Aliases:
  • addOutputOption
  • addOutputOptions
  • addOption
  • addOptions
  • withOutputOption
  • withOutputOptions
  • withOption
  • withOptions
  • outputOption

withInputOption(options)

withInputOptions(options)

withOption(options)

withOptions(options)

withOutputOption(options)

withOutputOptions(options)

Input methods

addInput(source)

fpsInput(fps)

fromFormat(format)

input(source)

Add an input to command

Also switches "current input", that is the input that will be affected by subsequent input-related methods.

Note: only one stream input is supported for now.

Parameters:
Name Type Description
source String | Readable

input file path or readable stream

Source:
Returns:

FfmpegCommand

Aliases:
  • mergeAdd
  • addInput

inputFormat(format)

Specify input format for the last specified input

Parameters:
Name Type Description
format String

input format

Source:
Returns:

FfmpegCommand

Aliases:
  • withInputFormat
  • fromFormat

inputFps(fps)

Specify input FPS for the last specified input (only valid for raw video formats)

Parameters:
Name Type Description
fps Number

input FPS

Source:
Returns:

FfmpegCommand

Aliases:
  • withInputFps
  • withInputFPS
  • withFpsInput
  • withFPSInput
  • inputFPS
  • inputFps
  • fpsInput

inputFPS(fps)

inputFps(fps)

loop(duration)

Loop over the last specified input

Parameters:
Name Type Argument Description
duration String | Number <optional>

loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string

Source:
Returns:

FfmpegCommand

mergeAdd(source)

native()

Use native framerate for the last specified input

Source:
Returns:

FfmmegCommand

Aliases:
  • nativeFramerate
  • withNativeFramerate

nativeFramerate()

seek(seek)

Specify output seek time

Parameters:
Name Type Description
seek String | Number

seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string

Source:
Returns:

FfmpegCommand

Alias:
seekOutput

seekInput(seek)

Specify input seek time for the last specified input

Parameters:
Name Type Description
seek String | Number

seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string

Source:
Returns:

FfmpegCommand

Aliases:
  • setStartTime
  • seekTo

seekOutput(seek)

Alias for FfmpegCommand#seek

seekTo(seek)

setStartTime(seek)

withFpsInput(fps)

withFPSInput(fps)

withInputFormat(format)

withInputFPS(fps)

withInputFps(fps)

withNativeFramerate()

Metadata methods

ffprobe(index, options, callback)

Run ffprobe on last specified input

Parameters:
Name Type Argument Description
index Number <optional>
<nullable>

0-based index of input to probe (defaults to last input)

options Array.<String> <optional>
<nullable>

array of output options to return

callback FfmpegCommand~ffprobeCallback

callback function

Source:

Miscellaneous methods

preset(preset)

Use preset

Parameters:
Name Type Description
preset String | function

preset name or preset function

Source:
Alias:
usingPreset

usingPreset(preset)

Other methods

clone()

Clone an ffmpeg command

This method is useful when you want to process the same input multiple times. It returns a new FfmpegCommand instance with the exact same options.

All options set after the clone() call will only be applied to the instance it has been called on.

Source:
Returns:

FfmpegCommand

Example:
var command = ffmpeg('/path/to/source.avi')
    .audioCodec('libfaac')
    .videoCodec('libx264')
    .format('mp4');

  command.clone()
    .size('320x200')
    .save('/path/to/output-small.mp4');

  command.clone()
    .size('640x400')
    .save('/path/to/output-medium.mp4');

  command.save('/path/to/output-original-size.mp4');

setFfmpegPath(ffmpegPath)

Manually define the ffmpeg binary full path.

Parameters:
Name Type Description
ffmpegPath String

The full path to the ffmpeg binary.

Source:
Returns:

FfmpegCommand

setFfprobePath(ffprobePath)

Manually define the ffprobe binary full path.

Parameters:
Name Type Description
ffprobePath String

The full path to the ffprobe binary.

Source:
Returns:

FfmpegCommand

setFlvtoolPath(flvtool)

Manually define the flvtool2/flvmeta binary full path.

Parameters:
Name Type Description
flvtool String

The full path to the flvtool2 or flvmeta binary.

Source:
Returns:

FfmpegCommand

Output methods

addOutput(target, pipeopts)

duration(duration)

Set output duration

Parameters:
Name Type Description
duration String | Number

duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string

Source:
Returns:

FfmpegCommand

Aliases:
  • withDuration
  • setDuration

flvmeta()

Run flvtool2/flvmeta on output

Source:
Returns:

FfmpegCommand

Alias:
updateFlvMetadata

format(format)

Set output format

Parameters:
Name Type Description
format String

output format name

Source:
Returns:

FfmpegCommand

Aliases:
  • toFormat
  • withOutputFormat
  • outputFormat

map(spec)

Add stream mapping to output

Parameters:
Name Type Description
spec String

stream specification string, with optional square brackets

Source:
Returns:

FfmpegCommand

output(target, pipeopts)

Add output

Parameters:
Name Type Argument Default Description
target String | Writable

target file path or writable stream

pipeopts Object <optional>
{}

pipe options (only applies to streams)

Source:
Returns:

FfmpegCommand

Alias:
addOutput

outputFormat(format)

setDuration(duration)

toFormat(format)

updateFlvMetadata()

withDuration(duration)

withOutputFormat(format)

Processing methods

concat(target, options)

Merge (concatenate) inputs to a single file

Parameters:
Name Type Argument Description
target String | Writable

output file or writable stream

options Object <optional>

pipe options (only used when outputting to a writable stream)

Source:
Returns:

FfmpegCommand

Aliases:
  • concatenate
  • mergeToFile

concatenate(target, options)

exec()

Alias for FfmpegCommand#run

execute()

Alias for FfmpegCommand#run

kill(signal)

Kill current ffmpeg process, if any

Parameters:
Name Type Argument Default Description
signal String <optional>
SIGKILL

signal name

Source:
Returns:

FfmpegCommand

mergeToFile(target, options)

pipe(stream, options)

Execute ffmpeg command and save output to a stream

If 'stream' is not specified, a PassThrough stream is created and returned. 'options' will be used when piping ffmpeg output to the output stream (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)

Parameters:
Name Type Argument Default Description
stream stream.Writable <optional>

output stream

options Object <optional>
{}

pipe options

Source:
Returns:

Output stream

Aliases:
  • stream
  • writeToStream

renice(niceness)

Renice current and/or future ffmpeg processes

Ignored on Windows platforms.

Parameters:
Name Type Argument Default Description
niceness Number <optional>
0

niceness value between -20 (highest priority) and 20 (lowest priority)

Source:
Returns:

FfmpegCommand

run()

Run ffmpeg command

Source:
Aliases:
  • exec
  • execute

save(output)

Execute ffmpeg command and save output to a file

Parameters:
Name Type Description
output String

file path

Source:
Returns:

FfmpegCommand

Alias:
saveToFile

saveToFile(output)

Alias for FfmpegCommand#save

screenshot(config, folder)

screenshots(config, folder)

Generate images from a video

Note: this method makes the command emit a 'filenames' event with an array of the generated image filenames.

Parameters:
Name Type Argument Default Description
config Number | Object <optional>
1

screenshot count or configuration object with the following keys:

Properties
Name Type Argument Default Description
count Number <optional>

number of screenshots to take; using this option takes screenshots at regular intervals (eg. count=4 would take screens at 20%, 40%, 60% and 80% of the video length).

folder String <optional>
'.'

output folder

filename String <optional>
'tn.png'

output filename pattern, may contain the following tokens:

  • '%s': offset in seconds
  • '%w': screenshot width
  • '%h': screenshot height
  • '%r': screenshot resolution (same as '%wx%h')
  • '%f': input filename
  • '%b': input basename (filename w/o extension)
  • '%i': index of screenshot in timemark array (can be zero-padded by using it like %000i)
timemarks Array.<Number> | Array.<String> <optional>

array of timemarks to take screenshots at; each timemark may be a number of seconds, a '[[hh:]mm:]ss[.xxx]' string or a 'XX%' string. Overrides 'count' if present.

timestamps Array.<Number> | Array.<String> <optional>

alias for 'timemarks'

fastSeek Boolean <optional>

use fast seek (less accurate)

size String <optional>

screenshot size, with the same syntax as FfmpegCommand#size

folder String <optional>

output folder (legacy alias for 'config.folder')

Source:
Returns:

FfmpegCommand

Aliases:
  • takeScreenshots
  • thumbnail
  • thumbnails
  • screenshot

stream(stream, options)

Alias for FfmpegCommand#pipe

takeScreenshots(config, folder)

thumbnail(config, folder)

thumbnails(config, folder)

writeToStream(stream, options)

Alias for FfmpegCommand#pipe

Video methods

fps(fps)

Specify output FPS

Parameters:
Name Type Description
fps Number

output FPS

Source:
Returns:

FfmpegCommand

Aliases:
  • withOutputFps
  • withOutputFPS
  • withFpsOutput
  • withFPSOutput
  • withFps
  • withFPS
  • outputFPS
  • outputFps
  • fpsOutput
  • FPSOutput
  • FPS

FPS(fps)

Alias for FfmpegCommand#fps

fpsOutput(fps)

Alias for FfmpegCommand#fps

FPSOutput(fps)

Alias for FfmpegCommand#fps

frames(frames)

Only transcode a certain number of frames

Parameters:
Name Type Description
frames Number

frame count

Source:
Returns:

FfmpegCommand

Aliases:
  • takeFrames
  • withFrames

noVideo()

Disable video in the output

Source:
Returns:

FfmpegCommand

Alias:
withNoVideo

outputFps(fps)

Alias for FfmpegCommand#fps

outputFPS(fps)

Alias for FfmpegCommand#fps

takeFrames(frames)

videoBitrate(bitrate, constant)

Specify video bitrate

Parameters:
Name Type Argument Default Description
bitrate String | Number

video bitrate in kbps (with an optional 'k' suffix)

constant Boolean <optional>
false

enforce constant bitrate

Source:
Returns:

FfmpegCommand

Alias:
withVideoBitrate

videoCodec(codec)

Specify video codec

Parameters:
Name Type Description
codec String

video codec name

Source:
Returns:

FfmpegCommand

Alias:
withVideoCodec

videoFilter(filters)

videoFilters(filters)

Specify custom video filter(s)

Can be called both with one or many filters, or a filter array.

Parameters:
Name Type Description
filters String | Array.<String> | Array.<Object>

video filter strings, string array or filter specification array, each with the following properties:

Properties
Name Type Argument Description
filter String

filter name

options String | Array.<String> | Object <optional>

filter option string, array, or object

Source:
Returns:

FfmpegCommand

Examples:
command.videoFilters('filter1');
command.videoFilters('filter1', 'filter2=param1=value1:param2=value2');
command.videoFilters(['filter1', 'filter2']);
command.videoFilters([
  {
    filter: 'filter1'
  },
  {
    filter: 'filter2',
    options: 'param=value:param=value'
  }
]);
command.videoFilters(
  {
    filter: 'filter1',
    options: ['value1', 'value2']
  },
  {
    filter: 'filter2',
    options: { param1: 'value1', param2: 'value2' }
  }
);
Aliases:
  • withVideoFilter
  • withVideoFilters
  • videoFilter

withFPS(fps)

Alias for FfmpegCommand#fps

withFps(fps)

Alias for FfmpegCommand#fps

withFPSOutput(fps)

Alias for FfmpegCommand#fps

withFpsOutput(fps)

Alias for FfmpegCommand#fps

withFrames(frames)

withNoVideo()

withOutputFps(fps)

Alias for FfmpegCommand#fps

withOutputFPS(fps)

Alias for FfmpegCommand#fps

withVideoBitrate(bitrate, constant)

withVideoCodec(codec)

withVideoFilter(filters)

withVideoFilters(filters)

Video size methods

applyAutoPad(pad, color)

applyAutopad(pad, color)

applyAutoPadding(pad, color)

applyAutopadding(pad, color)

aspect(aspect)

Set output aspect ratio

Parameters:
Name Type Description
aspect String | Number

aspect ratio (number or 'X:Y' string)

Source:
Returns:

FfmpegCommand

Aliases:
  • withAspect
  • withAspectRatio
  • setAspect
  • setAspectRatio
  • aspectRatio

aspectRatio(aspect)

autopad(pad, color)

Enable auto-padding the output

Parameters:
Name Type Argument Default Description
pad Boolean <optional>
true

enable/disable auto-padding

color String <optional>
'black'

pad color

Source:
Aliases:
  • applyAutopadding
  • applyAutoPadding
  • applyAutopad
  • applyAutoPad
  • withAutopadding
  • withAutoPadding
  • withAutopad
  • withAutoPad
  • autoPad

autoPad(pad, color)

keepDAR()

Keep display aspect ratio

This method is useful when converting an input with non-square pixels to an output format that does not support non-square pixels. It rescales the input so that the display aspect ratio is the same.

Source:
Returns:

FfmpegCommand

Aliases:
  • keepPixelAspect
  • keepDisplayAspect
  • keepDisplayAspectRatio

keepDisplayAspect()

keepDisplayAspectRatio()

keepPixelAspect()

setAspect(aspect)

setAspectRatio(aspect)

setSize(size)

Alias for FfmpegCommand#size

size(size)

Set output size

The 'size' parameter can have one of 4 forms:

  • 'X%': rescale to xx % of the original size
  • 'WxH': specify width and height
  • 'Wx?': specify width and compute height from input aspect ratio
  • '?xH': specify height and compute width from input aspect ratio

Note: both dimensions will be truncated to multiples of 2.

Parameters:
Name Type Description
size String

size string, eg. '33%', '320x240', '320x?', '?x240'

Source:
Returns:

FfmpegCommand

Aliases:
  • withSize
  • setSize

withAspect(aspect)

withAspectRatio(aspect)

withAutoPad(pad, color)

withAutopad(pad, color)

withAutopadding(pad, color)

withAutoPadding(pad, color)

withSize(size)

Alias for FfmpegCommand#size

Type Definitions

codecCallback(err, codecs)

A callback passed to FfmpegCommand#availableCodecs.

Parameters:
Name Type Description
err Error | null

error object or null if no error happened

codecs Object

codec object with codec names as keys and the following properties for each codec (more properties may be available depending on the ffmpeg version used):

Properties
Name Type Description
description String

codec description

canDecode Boolean

whether the codec is able to decode streams

canEncode Boolean

whether the codec is able to encode streams

Source:

encodersCallback(err, encoders)

A callback passed to FfmpegCommand#availableEncoders.

Parameters:
Name Type Description
err Error | null

error object or null if no error happened

encoders Object

encoders object with encoder names as keys and the following properties for each encoder:

Properties
Name Type Description
description String

codec description

type Boolean

"audio", "video" or "subtitle"

frameMT Boolean

whether the encoder is able to do frame-level multithreading

sliceMT Boolean

whether the encoder is able to do slice-level multithreading

experimental Boolean

whether the encoder is experimental

drawHorizBand Boolean

whether the encoder supports draw_horiz_band

directRendering Boolean

whether the encoder supports direct encoding method 1

Source:

ffprobeCallback(err, ffprobeData)

A callback passed to the FfmpegCommand#ffprobe method.

Parameters:
Name Type Description
err Error | null

error object or null if no error happened

ffprobeData Object

ffprobe output data; this object has the same format as what the following command returns:

`ffprobe -print_format json -show_streams -show_format INPUTFILE`
Properties
Name Type Description
streams Array

stream information

format Object

format information

Source:

filterCallback(err, filters)

A callback passed to FfmpegCommand#availableFilters.

Parameters:
Name Type Description
err Error | null

error object or null if no error happened

filters Object

filter object with filter names as keys and the following properties for each filter:

Properties
Name Type Description
description String

filter description

input String

input type, one of 'audio', 'video' and 'none'

multipleInputs Boolean

whether the filter supports multiple inputs

output String

output type, one of 'audio', 'video' and 'none'

multipleOutputs Boolean

whether the filter supports multiple outputs

Source:

formatCallback(err, formats)

A callback passed to FfmpegCommand#availableFormats.

Parameters:
Name Type Description
err Error | null

error object or null if no error happened

formats Object

format object with format names as keys and the following properties for each format:

Properties
Name Type Description
description String

format description

canDemux Boolean

whether the format is able to demux streams from an input file

canMux Boolean

whether the format is able to mux streams into an output file

Source:

Events

codecData

Emitted when ffmpeg reports input codec data

Parameters:
Name Type Description
codecData Object

codec data object

Properties
Name Type Description
format String

input format name

audio String

input audio codec name

audio_details String

input audio codec parameters

video String

input video codec name

video_details String

input video codec parameters

Source:

end

Emitted when a command finishes processing

Parameters:
Name Type Argument Description
filenames|stdout Array | String | null <optional>

generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise

stderr String | null

ffmpeg stderr

Source:

error

Emitted when an error happens when preparing or running a command

Parameters:
Name Type Description
error Error

error object

stdout String | null

ffmpeg stdout, unless outputting to a stream

stderr String | null

ffmpeg stderr

Source:

progress

Emitted when ffmpeg reports progress information

Parameters:
Name Type Description
progress Object

progress object

Properties
Name Type Argument Description
frames Number

number of frames transcoded

currentFps Number

current processing speed in frames per second

currentKbps Number

current output generation speed in kilobytes per second

targetSize Number

current output file size

timemark String

current video timemark

percent Number <optional>

processing progress (may not be available depending on input)

Source:

start

Emitted just after ffmpeg has been spawned.

Parameters:
Name Type Description
command String

ffmpeg command line

Source:

stderr

Emitted when ffmpeg outputs to stderr

Parameters:
Name Type Description
line String

stderr output line

Source:

================================================ FILE: doc/audio.js.html ================================================ JSDoc: Source: options/audio.js

Source: options/audio.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Audio-related methods
 */

module.exports = function(proto) {
  /**
   * Disable audio in the output
   *
   * @method FfmpegCommand#noAudio
   * @category Audio
   * @aliases withNoAudio
   * @return FfmpegCommand
   */
  proto.withNoAudio =
  proto.noAudio = function() {
    this._currentOutput.audio.clear();
    this._currentOutput.audioFilters.clear();
    this._currentOutput.audio('-an');

    return this;
  };


  /**
   * Specify audio codec
   *
   * @method FfmpegCommand#audioCodec
   * @category Audio
   * @aliases withAudioCodec
   *
   * @param {String} codec audio codec name
   * @return FfmpegCommand
   */
  proto.withAudioCodec =
  proto.audioCodec = function(codec) {
    this._currentOutput.audio('-acodec', codec);

    return this;
  };


  /**
   * Specify audio bitrate
   *
   * @method FfmpegCommand#audioBitrate
   * @category Audio
   * @aliases withAudioBitrate
   *
   * @param {String|Number} bitrate audio bitrate in kbps (with an optional 'k' suffix)
   * @return FfmpegCommand
   */
  proto.withAudioBitrate =
  proto.audioBitrate = function(bitrate) {
    this._currentOutput.audio('-b:a', ('' + bitrate).replace(/k?$/, 'k'));
    return this;
  };


  /**
   * Specify audio channel count
   *
   * @method FfmpegCommand#audioChannels
   * @category Audio
   * @aliases withAudioChannels
   *
   * @param {Number} channels channel count
   * @return FfmpegCommand
   */
  proto.withAudioChannels =
  proto.audioChannels = function(channels) {
    this._currentOutput.audio('-ac', channels);
    return this;
  };


  /**
   * Specify audio frequency
   *
   * @method FfmpegCommand#audioFrequency
   * @category Audio
   * @aliases withAudioFrequency
   *
   * @param {Number} freq audio frequency in Hz
   * @return FfmpegCommand
   */
  proto.withAudioFrequency =
  proto.audioFrequency = function(freq) {
    this._currentOutput.audio('-ar', freq);
    return this;
  };


  /**
   * Specify audio quality
   *
   * @method FfmpegCommand#audioQuality
   * @category Audio
   * @aliases withAudioQuality
   *
   * @param {Number} quality audio quality factor
   * @return FfmpegCommand
   */
  proto.withAudioQuality =
  proto.audioQuality = function(quality) {
    this._currentOutput.audio('-aq', quality);
    return this;
  };


  /**
   * Specify custom audio filter(s)
   *
   * Can be called both with one or many filters, or a filter array.
   *
   * @example
   * command.audioFilters('filter1');
   *
   * @example
   * command.audioFilters('filter1', 'filter2=param1=value1:param2=value2');
   *
   * @example
   * command.audioFilters(['filter1', 'filter2']);
   *
   * @example
   * command.audioFilters([
   *   {
   *     filter: 'filter1'
   *   },
   *   {
   *     filter: 'filter2',
   *     options: 'param=value:param=value'
   *   }
   * ]);
   *
   * @example
   * command.audioFilters(
   *   {
   *     filter: 'filter1',
   *     options: ['value1', 'value2']
   *   },
   *   {
   *     filter: 'filter2',
   *     options: { param1: 'value1', param2: 'value2' }
   *   }
   * );
   *
   * @method FfmpegCommand#audioFilters
   * @aliases withAudioFilter,withAudioFilters,audioFilter
   * @category Audio
   *
   * @param {...String|String[]|Object[]} filters audio filter strings, string array or
   *   filter specification array, each with the following properties:
   * @param {String} filters.filter filter name
   * @param {String|String[]|Object} [filters.options] filter option string, array, or object
   * @return FfmpegCommand
   */
  proto.withAudioFilter =
  proto.withAudioFilters =
  proto.audioFilter =
  proto.audioFilters = function(filters) {
    if (arguments.length > 1) {
      filters = [].slice.call(arguments);
    }

    if (!Array.isArray(filters)) {
      filters = [filters];
    }

    this._currentOutput.audioFilters(utils.makeFilterStrings(filters));
    return this;
  };
};

================================================ FILE: doc/capabilities.js.html ================================================ JSDoc: Source: capabilities.js

Source: capabilities.js

/*jshint node:true*/
'use strict';

var fs = require('fs');
var path = require('path');
var async = require('async');
var utils = require('./utils');

/*
 *! Capability helpers
 */

var avCodecRegexp = /^\s*([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/;
var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/;
var ffEncodersRegexp = /\(encoders:([^\)]+)\)/;
var ffDecodersRegexp = /\(decoders:([^\)]+)\)/;
var encodersRegexp = /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/;
var formatRegexp = /^\s*([D ])([E ])\s+([^ ]+)\s+(.*)$/;
var lineBreakRegexp = /\r\n|\r|\n/;
var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/;

var cache = {};

module.exports = function(proto) {
  /**
   * Manually define the ffmpeg binary full path.
   *
   * @method FfmpegCommand#setFfmpegPath
   *
   * @param {String} ffmpegPath The full path to the ffmpeg binary.
   * @return FfmpegCommand
   */
  proto.setFfmpegPath = function(ffmpegPath) {
    cache.ffmpegPath = ffmpegPath;
    return this;
  };

  /**
   * Manually define the ffprobe binary full path.
   *
   * @method FfmpegCommand#setFfprobePath
   *
   * @param {String} ffprobePath The full path to the ffprobe binary.
   * @return FfmpegCommand
   */
  proto.setFfprobePath = function(ffprobePath) {
    cache.ffprobePath = ffprobePath;
    return this;
  };

  /**
   * Manually define the flvtool2/flvmeta binary full path.
   *
   * @method FfmpegCommand#setFlvtoolPath
   *
   * @param {String} flvtool The full path to the flvtool2 or flvmeta binary.
   * @return FfmpegCommand
   */
  proto.setFlvtoolPath = function(flvtool) {
    cache.flvtoolPath = flvtool;
    return this;
  };

  /**
   * Forget executable paths
   *
   * (only used for testing purposes)
   *
   * @method FfmpegCommand#_forgetPaths
   * @private
   */
  proto._forgetPaths = function() {
    delete cache.ffmpegPath;
    delete cache.ffprobePath;
    delete cache.flvtoolPath;
  };

  /**
   * Check for ffmpeg availability
   *
   * If the FFMPEG_PATH environment variable is set, try to use it.
   * If it is unset or incorrect, try to find ffmpeg in the PATH instead.
   *
   * @method FfmpegCommand#_getFfmpegPath
   * @param {Function} callback callback with signature (err, path)
   * @private
   */
  proto._getFfmpegPath = function(callback) {
    if ('ffmpegPath' in cache) {
      return callback(null, cache.ffmpegPath);
    }

    async.waterfall([
      // Try FFMPEG_PATH
      function(cb) {
        if (process.env.FFMPEG_PATH) {
          fs.exists(process.env.FFMPEG_PATH, function(exists) {
            if (exists) {
              cb(null, process.env.FFMPEG_PATH);
            } else {
              cb(null, '');
            }
          });
        } else {
          cb(null, '');
        }
      },

      // Search in the PATH
      function(ffmpeg, cb) {
        if (ffmpeg.length) {
          return cb(null, ffmpeg);
        }

        utils.which('ffmpeg', function(err, ffmpeg) {
          cb(err, ffmpeg);
        });
      }
    ], function(err, ffmpeg) {
      if (err) {
        callback(err);
      } else {
        callback(null, cache.ffmpegPath = (ffmpeg || ''));
      }
    });
  };


  /**
   * Check for ffprobe availability
   *
   * If the FFPROBE_PATH environment variable is set, try to use it.
   * If it is unset or incorrect, try to find ffprobe in the PATH instead.
   * If this still fails, try to find ffprobe in the same directory as ffmpeg.
   *
   * @method FfmpegCommand#_getFfprobePath
   * @param {Function} callback callback with signature (err, path)
   * @private
   */
  proto._getFfprobePath = function(callback) {
    var self = this;

    if ('ffprobePath' in cache) {
      return callback(null, cache.ffprobePath);
    }

    async.waterfall([
      // Try FFPROBE_PATH
      function(cb) {
        if (process.env.FFPROBE_PATH) {
          fs.exists(process.env.FFPROBE_PATH, function(exists) {
            cb(null, exists ? process.env.FFPROBE_PATH : '');
          });
        } else {
          cb(null, '');
        }
      },

      // Search in the PATH
      function(ffprobe, cb) {
        if (ffprobe.length) {
          return cb(null, ffprobe);
        }

        utils.which('ffprobe', function(err, ffprobe) {
          cb(err, ffprobe);
        });
      },

      // Search in the same directory as ffmpeg
      function(ffprobe, cb) {
        if (ffprobe.length) {
          return cb(null, ffprobe);
        }

        self._getFfmpegPath(function(err, ffmpeg) {
          if (err) {
            cb(err);
          } else if (ffmpeg.length) {
            var name = utils.isWindows ? 'ffprobe.exe' : 'ffprobe';
            var ffprobe = path.join(path.dirname(ffmpeg), name);
            fs.exists(ffprobe, function(exists) {
              cb(null, exists ? ffprobe : '');
            });
          } else {
            cb(null, '');
          }
        });
      }
    ], function(err, ffprobe) {
      if (err) {
        callback(err);
      } else {
        callback(null, cache.ffprobePath = (ffprobe || ''));
      }
    });
  };


  /**
   * Check for flvtool2/flvmeta availability
   *
   * If the FLVTOOL2_PATH or FLVMETA_PATH environment variable are set, try to use them.
   * If both are either unset or incorrect, try to find flvtool2 or flvmeta in the PATH instead.
   *
   * @method FfmpegCommand#_getFlvtoolPath
   * @param {Function} callback callback with signature (err, path)
   * @private
   */
   proto._getFlvtoolPath = function(callback) {
    if ('flvtoolPath' in cache) {
      return callback(null, cache.flvtoolPath);
    }

    async.waterfall([
      // Try FLVMETA_PATH
      function(cb) {
        if (process.env.FLVMETA_PATH) {
          fs.exists(process.env.FLVMETA_PATH, function(exists) {
            cb(null, exists ? process.env.FLVMETA_PATH : '');
          });
        } else {
          cb(null, '');
        }
      },

      // Try FLVTOOL2_PATH
      function(flvtool, cb) {
        if (flvtool.length) {
          return cb(null, flvtool);
        }

        if (process.env.FLVTOOL2_PATH) {
          fs.exists(process.env.FLVTOOL2_PATH, function(exists) {
            cb(null, exists ? process.env.FLVTOOL2_PATH : '');
          });
        } else {
          cb(null, '');
        }
      },

      // Search for flvmeta in the PATH
      function(flvtool, cb) {
        if (flvtool.length) {
          return cb(null, flvtool);
        }

        utils.which('flvmeta', function(err, flvmeta) {
          cb(err, flvmeta);
        });
      },

      // Search for flvtool2 in the PATH
      function(flvtool, cb) {
        if (flvtool.length) {
          return cb(null, flvtool);
        }

        utils.which('flvtool2', function(err, flvtool2) {
          cb(err, flvtool2);
        });
      },
    ], function(err, flvtool) {
      if (err) {
        callback(err);
      } else {
        callback(null, cache.flvtoolPath = (flvtool || ''));
      }
    });
  };


  /**
   * A callback passed to {@link FfmpegCommand#availableFilters}.
   *
   * @callback FfmpegCommand~filterCallback
   * @param {Error|null} err error object or null if no error happened
   * @param {Object} filters filter object with filter names as keys and the following
   *   properties for each filter:
   * @param {String} filters.description filter description
   * @param {String} filters.input input type, one of 'audio', 'video' and 'none'
   * @param {Boolean} filters.multipleInputs whether the filter supports multiple inputs
   * @param {String} filters.output output type, one of 'audio', 'video' and 'none'
   * @param {Boolean} filters.multipleOutputs whether the filter supports multiple outputs
   */

  /**
   * Query ffmpeg for available filters
   *
   * @method FfmpegCommand#availableFilters
   * @category Capabilities
   * @aliases getAvailableFilters
   *
   * @param {FfmpegCommand~filterCallback} callback callback function
   */
  proto.availableFilters =
  proto.getAvailableFilters = function(callback) {
    if ('filters' in cache) {
      return callback(null, cache.filters);
    }

    this._spawnFfmpeg(['-filters'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) {
      if (err) {
        return callback(err);
      }

      var stdout = stdoutRing.get();
      var lines = stdout.split('\n');
      var data = {};
      var types = { A: 'audio', V: 'video', '|': 'none' };

      lines.forEach(function(line) {
        var match = line.match(filterRegexp);
        if (match) {
          data[match[1]] = {
            description: match[4],
            input: types[match[2].charAt(0)],
            multipleInputs: match[2].length > 1,
            output: types[match[3].charAt(0)],
            multipleOutputs: match[3].length > 1
          };
        }
      });

      callback(null, cache.filters = data);
    });
  };


  /**
   * A callback passed to {@link FfmpegCommand#availableCodecs}.
   *
   * @callback FfmpegCommand~codecCallback
   * @param {Error|null} err error object or null if no error happened
   * @param {Object} codecs codec object with codec names as keys and the following
   *   properties for each codec (more properties may be available depending on the
   *   ffmpeg version used):
   * @param {String} codecs.description codec description
   * @param {Boolean} codecs.canDecode whether the codec is able to decode streams
   * @param {Boolean} codecs.canEncode whether the codec is able to encode streams
   */

  /**
   * Query ffmpeg for available codecs
   *
   * @method FfmpegCommand#availableCodecs
   * @category Capabilities
   * @aliases getAvailableCodecs
   *
   * @param {FfmpegCommand~codecCallback} callback callback function
   */
  proto.availableCodecs =
  proto.getAvailableCodecs = function(callback) {
    if ('codecs' in cache) {
      return callback(null, cache.codecs);
    }

    this._spawnFfmpeg(['-codecs'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) {
      if (err) {
        return callback(err);
      }

      var stdout = stdoutRing.get();
      var lines = stdout.split(lineBreakRegexp);
      var data = {};

      lines.forEach(function(line) {
        var match = line.match(avCodecRegexp);
        if (match && match[7] !== '=') {
          data[match[7]] = {
            type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]],
            description: match[8],
            canDecode: match[1] === 'D',
            canEncode: match[2] === 'E',
            drawHorizBand: match[4] === 'S',
            directRendering: match[5] === 'D',
            weirdFrameTruncation: match[6] === 'T'
          };
        }

        match = line.match(ffCodecRegexp);
        if (match && match[7] !== '=') {
          var codecData = data[match[7]] = {
            type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]],
            description: match[8],
            canDecode: match[1] === 'D',
            canEncode: match[2] === 'E',
            intraFrameOnly: match[4] === 'I',
            isLossy: match[5] === 'L',
            isLossless: match[6] === 'S'
          };

          var encoders = codecData.description.match(ffEncodersRegexp);
          encoders = encoders ? encoders[1].trim().split(' ') : [];

          var decoders = codecData.description.match(ffDecodersRegexp);
          decoders = decoders ? decoders[1].trim().split(' ') : [];

          if (encoders.length || decoders.length) {
            var coderData = {};
            utils.copy(codecData, coderData);
            delete coderData.canEncode;
            delete coderData.canDecode;

            encoders.forEach(function(name) {
              data[name] = {};
              utils.copy(coderData, data[name]);
              data[name].canEncode = true;
            });

            decoders.forEach(function(name) {
              if (name in data) {
                data[name].canDecode = true;
              } else {
                data[name] = {};
                utils.copy(coderData, data[name]);
                data[name].canDecode = true;
              }
            });
          }
        }
      });

      callback(null, cache.codecs = data);
    });
  };


  /**
   * A callback passed to {@link FfmpegCommand#availableEncoders}.
   *
   * @callback FfmpegCommand~encodersCallback
   * @param {Error|null} err error object or null if no error happened
   * @param {Object} encoders encoders object with encoder names as keys and the following
   *   properties for each encoder:
   * @param {String} encoders.description codec description
   * @param {Boolean} encoders.type "audio", "video" or "subtitle"
   * @param {Boolean} encoders.frameMT whether the encoder is able to do frame-level multithreading
   * @param {Boolean} encoders.sliceMT whether the encoder is able to do slice-level multithreading
   * @param {Boolean} encoders.experimental whether the encoder is experimental
   * @param {Boolean} encoders.drawHorizBand whether the encoder supports draw_horiz_band
   * @param {Boolean} encoders.directRendering whether the encoder supports direct encoding method 1
   */

  /**
   * Query ffmpeg for available encoders
   *
   * @method FfmpegCommand#availableEncoders
   * @category Capabilities
   * @aliases getAvailableEncoders
   *
   * @param {FfmpegCommand~encodersCallback} callback callback function
   */
  proto.availableEncoders =
  proto.getAvailableEncoders = function(callback) {
    if ('encoders' in cache) {
      return callback(null, cache.encoders);
    }

    this._spawnFfmpeg(['-encoders'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) {
      if (err) {
        return callback(err);
      }

      var stdout = stdoutRing.get();
      var lines = stdout.split(lineBreakRegexp);
      var data = {};

      lines.forEach(function(line) {
        var match = line.match(encodersRegexp);
        if (match && match[7] !== '=') {
          data[match[7]] = {
            type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[1]],
            description: match[8],
            frameMT: match[2] === 'F',
            sliceMT: match[3] === 'S',
            experimental: match[4] === 'X',
            drawHorizBand: match[5] === 'B',
            directRendering: match[6] === 'D'
          };
        }
      });

      callback(null, cache.encoders = data);
    });
  };


  /**
   * A callback passed to {@link FfmpegCommand#availableFormats}.
   *
   * @callback FfmpegCommand~formatCallback
   * @param {Error|null} err error object or null if no error happened
   * @param {Object} formats format object with format names as keys and the following
   *   properties for each format:
   * @param {String} formats.description format description
   * @param {Boolean} formats.canDemux whether the format is able to demux streams from an input file
   * @param {Boolean} formats.canMux whether the format is able to mux streams into an output file
   */

  /**
   * Query ffmpeg for available formats
   *
   * @method FfmpegCommand#availableFormats
   * @category Capabilities
   * @aliases getAvailableFormats
   *
   * @param {FfmpegCommand~formatCallback} callback callback function
   */
  proto.availableFormats =
  proto.getAvailableFormats = function(callback) {
    if ('formats' in cache) {
      return callback(null, cache.formats);
    }

    // Run ffmpeg -formats
    this._spawnFfmpeg(['-formats'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) {
      if (err) {
        return callback(err);
      }

      // Parse output
      var stdout = stdoutRing.get();
      var lines = stdout.split(lineBreakRegexp);
      var data = {};

      lines.forEach(function(line) {
        var match = line.match(formatRegexp);
        if (match) {
          match[3].split(',').forEach(function(format) {
            if (!(format in data)) {
              data[format] = {
                description: match[4],
                canDemux: false,
                canMux: false
              };
            }

            if (match[1] === 'D') {
              data[format].canDemux = true;
            }
            if (match[2] === 'E') {
              data[format].canMux = true;
            }
          });
        }
      });

      callback(null, cache.formats = data);
    });
  };


  /**
   * Check capabilities before executing a command
   *
   * Checks whether all used codecs and formats are indeed available
   *
   * @method FfmpegCommand#_checkCapabilities
   * @param {Function} callback callback with signature (err)
   * @private
   */
  proto._checkCapabilities = function(callback) {
    var self = this;
    async.waterfall([
      // Get available formats
      function(cb) {
        self.availableFormats(cb);
      },

      // Check whether specified formats are available
      function(formats, cb) {
        var unavailable;

        // Output format(s)
        unavailable = self._outputs
          .reduce(function(fmts, output) {
            var format = output.options.find('-f', 1);
            if (format) {
              if (!(format[0] in formats) || !(formats[format[0]].canMux)) {
                fmts.push(format);
              }
            }

            return fmts;
          }, []);

        if (unavailable.length === 1) {
          return cb(new Error('Output format ' + unavailable[0] + ' is not available'));
        } else if (unavailable.length > 1) {
          return cb(new Error('Output formats ' + unavailable.join(', ') + ' are not available'));
        }

        // Input format(s)
        unavailable = self._inputs
          .reduce(function(fmts, input) {
            var format = input.options.find('-f', 1);
            if (format) {
              if (!(format[0] in formats) || !(formats[format[0]].canDemux)) {
                fmts.push(format[0]);
              }
            }

            return fmts;
          }, []);

        if (unavailable.length === 1) {
          return cb(new Error('Input format ' + unavailable[0] + ' is not available'));
        } else if (unavailable.length > 1) {
          return cb(new Error('Input formats ' + unavailable.join(', ') + ' are not available'));
        }

        cb();
      },

      // Get available codecs
      function(cb) {
        self.availableEncoders(cb);
      },

      // Check whether specified codecs are available and add strict experimental options if needed
      function(encoders, cb) {
        var unavailable;

        // Audio codec(s)
        unavailable = self._outputs.reduce(function(cdcs, output) {
          var acodec = output.audio.find('-acodec', 1);
          if (acodec && acodec[0] !== 'copy') {
            if (!(acodec[0] in encoders) || encoders[acodec[0]].type !== 'audio') {
              cdcs.push(acodec[0]);
            }
          }

          return cdcs;
        }, []);

        if (unavailable.length === 1) {
          return cb(new Error('Audio codec ' + unavailable[0] + ' is not available'));
        } else if (unavailable.length > 1) {
          return cb(new Error('Audio codecs ' + unavailable.join(', ') + ' are not available'));
        }

        // Video codec(s)
        unavailable = self._outputs.reduce(function(cdcs, output) {
          var vcodec = output.video.find('-vcodec', 1);
          if (vcodec && vcodec[0] !== 'copy') {
            if (!(vcodec[0] in encoders) || encoders[vcodec[0]].type !== 'video') {
              cdcs.push(vcodec[0]);
            }
          }

          return cdcs;
        }, []);

        if (unavailable.length === 1) {
          return cb(new Error('Video codec ' + unavailable[0] + ' is not available'));
        } else if (unavailable.length > 1) {
          return cb(new Error('Video codecs ' + unavailable.join(', ') + ' are not available'));
        }

        cb();
      }
    ], callback);
  };
};

================================================ FILE: doc/custom.js.html ================================================ JSDoc: Source: options/custom.js

Source: options/custom.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Custom options methods
 */

module.exports = function(proto) {
  /**
   * Add custom input option(s)
   *
   * When passing a single string or an array, each string containing two
   * words is split (eg. inputOptions('-option value') is supported) for
   * compatibility reasons.  This is not the case when passing more than
   * one argument.
   *
   * @example
   * command.inputOptions('option1');
   *
   * @example
   * command.inputOptions('option1', 'option2');
   *
   * @example
   * command.inputOptions(['option1', 'option2']);
   *
   * @method FfmpegCommand#inputOptions
   * @category Custom options
   * @aliases addInputOption,addInputOptions,withInputOption,withInputOptions,inputOption
   *
   * @param {...String} options option string(s) or string array
   * @return FfmpegCommand
   */
  proto.addInputOption =
  proto.addInputOptions =
  proto.withInputOption =
  proto.withInputOptions =
  proto.inputOption =
  proto.inputOptions = function(options) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    var doSplit = true;

    if (arguments.length > 1) {
      options = [].slice.call(arguments);
      doSplit = false;
    }

    if (!Array.isArray(options)) {
      options = [options];
    }

    this._currentInput.options(options.reduce(function(options, option) {
      var split = option.split(' ');

      if (doSplit && split.length === 2) {
        options.push(split[0], split[1]);
      } else {
        options.push(option);
      }

      return options;
    }, []));
    return this;
  };


  /**
   * Add custom output option(s)
   *
   * @example
   * command.outputOptions('option1');
   *
   * @example
   * command.outputOptions('option1', 'option2');
   *
   * @example
   * command.outputOptions(['option1', 'option2']);
   *
   * @method FfmpegCommand#outputOptions
   * @category Custom options
   * @aliases addOutputOption,addOutputOptions,addOption,addOptions,withOutputOption,withOutputOptions,withOption,withOptions,outputOption
   *
   * @param {...String} options option string(s) or string array
   * @return FfmpegCommand
   */
  proto.addOutputOption =
  proto.addOutputOptions =
  proto.addOption =
  proto.addOptions =
  proto.withOutputOption =
  proto.withOutputOptions =
  proto.withOption =
  proto.withOptions =
  proto.outputOption =
  proto.outputOptions = function(options) {
    var doSplit = true;

    if (arguments.length > 1) {
      options = [].slice.call(arguments);
      doSplit = false;
    }

    if (!Array.isArray(options)) {
      options = [options];
    }

    this._currentOutput.options(options.reduce(function(options, option) {
      var split = option.split(' ');

      if (doSplit && split.length === 2) {
        options.push(split[0], split[1]);
      } else {
        options.push(option);
      }

      return options;
    }, []));
    return this;
  };


  /**
   * Specify a complex filtergraph
   *
   * Calling this method will override any previously set filtergraph, but you can set
   * as many filters as needed in one call.
   *
   * @example <caption>Overlay an image over a video (using a filtergraph string)</caption>
   *   ffmpeg()
   *     .input('video.avi')
   *     .input('image.png')
   *     .complexFilter('[0:v][1:v]overlay[out]', ['out']);
   *
   * @example <caption>Overlay an image over a video (using a filter array)</caption>
   *   ffmpeg()
   *     .input('video.avi')
   *     .input('image.png')
   *     .complexFilter([{
   *       filter: 'overlay',
   *       inputs: ['0:v', '1:v'],
   *       outputs: ['out']
   *     }], ['out']);
   *
   * @example <caption>Split video into RGB channels and output a 3x1 video with channels side to side</caption>
   *  ffmpeg()
   *    .input('video.avi')
   *    .complexFilter([
   *      // Duplicate video stream 3 times into streams a, b, and c
   *      { filter: 'split', options: '3', outputs: ['a', 'b', 'c'] },
   *
   *      // Create stream 'red' by cancelling green and blue channels from stream 'a'
   *      { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' },
   *
   *      // Create stream 'green' by cancelling red and blue channels from stream 'b'
   *      { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' },
   *
   *      // Create stream 'blue' by cancelling red and green channels from stream 'c'
   *      { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' },
   *
   *      // Pad stream 'red' to 3x width, keeping the video on the left, and name output 'padded'
   *      { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' },
   *
   *      // Overlay 'green' onto 'padded', moving it to the center, and name output 'redgreen'
   *      { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen'},
   *
   *      // Overlay 'blue' onto 'redgreen', moving it to the right
   *      { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue']},
   *    ]);
   *
   * @method FfmpegCommand#complexFilter
   * @category Custom options
   * @aliases filterGraph
   *
   * @param {String|Array} spec filtergraph string or array of filter specification
   *   objects, each having the following properties:
   * @param {String} spec.filter filter name
   * @param {String|Array} [spec.inputs] (array of) input stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically choosing the first unused matching streams
   * @param {String|Array} [spec.outputs] (array of) output stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically assigning the output to the output file
   * @param {Object|String|Array} [spec.options] filter options, can be omitted to not set any options
   * @param {Array} [map] (array of) stream specifier(s) from the graph to include in
   *   ffmpeg output, defaults to ffmpeg automatically choosing the first matching streams.
   * @return FfmpegCommand
   */
  proto.filterGraph =
  proto.complexFilter = function(spec, map) {
    this._complexFilters.clear();

    if (!Array.isArray(spec)) {
      spec = [spec];
    }

    this._complexFilters('-filter_complex', utils.makeFilterStrings(spec).join(';'));

    if (Array.isArray(map)) {
      var self = this;
      map.forEach(function(streamSpec) {
        self._complexFilters('-map', streamSpec.replace(utils.streamRegexp, '[$1]'));
      });
    } else if (typeof map === 'string') {
      this._complexFilters('-map', map.replace(utils.streamRegexp, '[$1]'));
    }

    return this;
  };
};

================================================ FILE: doc/ffprobe.js.html ================================================ JSDoc: Source: ffprobe.js

Source: ffprobe.js

/*jshint node:true, laxcomma:true*/
'use strict';

var spawn = require('child_process').spawn;


function legacyTag(key) { return key.match(/^TAG:/); }
function legacyDisposition(key) { return key.match(/^DISPOSITION:/); }

function parseFfprobeOutput(out) {
  var lines = out.split(/\r\n|\r|\n/);

  lines = lines.filter(function (line) {
    return line.length > 0;
  });

  var data = {
    streams: [],
    format: {},
    chapters: []
  };

  function parseBlock(name) {
    var data = {};

    var line = lines.shift();
    while (typeof line !== 'undefined') {
      if (line.toLowerCase() == '[/'+name+']') {
        return data;
      } else if (line.match(/^\[/)) {
        line = lines.shift();
        continue;
      }

      var kv = line.match(/^([^=]+)=(.*)$/);
      if (kv) {
        if (!(kv[1].match(/^TAG:/)) && kv[2].match(/^[0-9]+(\.[0-9]+)?$/)) {
          data[kv[1]] = Number(kv[2]);
        } else {
          data[kv[1]] = kv[2];
        }
      }

      line = lines.shift();
    }

    return data;
  }

  var line = lines.shift();
  while (typeof line !== 'undefined') {
    if (line.match(/^\[stream/i)) {
      var stream = parseBlock('stream');
      data.streams.push(stream);
    } else if (line.match(/^\[chapter/i)) {
      var chapter = parseBlock('chapter');
      data.chapters.push(chapter);
    } else if (line.toLowerCase() === '[format]') {
      data.format = parseBlock('format');
    }

    line = lines.shift();
  }

  return data;
}



module.exports = function(proto) {
  /**
   * A callback passed to the {@link FfmpegCommand#ffprobe} method.
   *
   * @callback FfmpegCommand~ffprobeCallback
   *
   * @param {Error|null} err error object or null if no error happened
   * @param {Object} ffprobeData ffprobe output data; this object
   *   has the same format as what the following command returns:
   *
   *     `ffprobe -print_format json -show_streams -show_format INPUTFILE`
   * @param {Array} ffprobeData.streams stream information
   * @param {Object} ffprobeData.format format information
   */

  /**
   * Run ffprobe on last specified input
   *
   * @method FfmpegCommand#ffprobe
   * @category Metadata
   *
   * @param {?Number} [index] 0-based index of input to probe (defaults to last input)
   * @param {?String[]} [options] array of output options to return
   * @param {FfmpegCommand~ffprobeCallback} callback callback function
   *
   */
  proto.ffprobe = function() {
    var input, index = null, options = [], callback;

    // the last argument should be the callback
    var callback = arguments[arguments.length - 1];

    var ended = false
    function handleCallback(err, data) {
      if (!ended) {
        ended = true;
        callback(err, data);
      }
    };

    // map the arguments to the correct variable names
    switch (arguments.length) {
      case 3:
        index = arguments[0];
        options = arguments[1];
        break;
      case 2:
        if (typeof arguments[0] === 'number') {
          index = arguments[0];
        } else if (Array.isArray(arguments[0])) {
          options = arguments[0];
        }
        break;
    }


    if (index === null) {
      if (!this._currentInput) {
        return handleCallback(new Error('No input specified'));
      }

      input = this._currentInput;
    } else {
      input = this._inputs[index];

      if (!input) {
        return handleCallback(new Error('Invalid input index'));
      }
    }

    // Find ffprobe
    this._getFfprobePath(function(err, path) {
      if (err) {
        return handleCallback(err);
      } else if (!path) {
        return handleCallback(new Error('Cannot find ffprobe'));
      }

      var stdout = '';
      var stdoutClosed = false;
      var stderr = '';
      var stderrClosed = false;

      // Spawn ffprobe
      var src = input.isStream ? 'pipe:0' : input.source;
      var ffprobe = spawn(path, ['-show_streams', '-show_format'].concat(options, src));

      if (input.isStream) {
        // Skip errors on stdin. These get thrown when ffprobe is complete and
        // there seems to be no way hook in and close stdin before it throws.
        ffprobe.stdin.on('error', function(err) {
          if (['ECONNRESET', 'EPIPE'].indexOf(err.code) >= 0) { return; }
          handleCallback(err);
        });

        // Once ffprobe's input stream closes, we need no more data from the
        // input
        ffprobe.stdin.on('close', function() {
            input.source.pause();
            input.source.unpipe(ffprobe.stdin);
        });

        input.source.pipe(ffprobe.stdin);
      }

      ffprobe.on('error', callback);

      // Ensure we wait for captured streams to end before calling callback
      var exitError = null;
      function handleExit(err) {
        if (err) {
          exitError = err;
        }

        if (processExited && stdoutClosed && stderrClosed) {
          if (exitError) {
            if (stderr) {
              exitError.message += '\n' + stderr;
            }

            return handleCallback(exitError);
          }

          // Process output
          var data = parseFfprobeOutput(stdout);

          // Handle legacy output with "TAG:x" and "DISPOSITION:x" keys
          [data.format].concat(data.streams).forEach(function(target) {
            if (target) {
              var legacyTagKeys = Object.keys(target).filter(legacyTag);

              if (legacyTagKeys.length) {
                target.tags = target.tags || {};

                legacyTagKeys.forEach(function(tagKey) {
                  target.tags[tagKey.substr(4)] = target[tagKey];
                  delete target[tagKey];
                });
              }

              var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition);

              if (legacyDispositionKeys.length) {
                target.disposition = target.disposition || {};

                legacyDispositionKeys.forEach(function(dispositionKey) {
                  target.disposition[dispositionKey.substr(12)] = target[dispositionKey];
                  delete target[dispositionKey];
                });
              }
            }
          });

          handleCallback(null, data);
        }
      }

      // Handle ffprobe exit
      var processExited = false;
      ffprobe.on('exit', function(code, signal) {
        processExited = true;

        if (code) {
          handleExit(new Error('ffprobe exited with code ' + code));
        } else if (signal) {
          handleExit(new Error('ffprobe was killed with signal ' + signal));
        } else {
          handleExit();
        }
      });

      // Handle stdout/stderr streams
      ffprobe.stdout.on('data', function(data) {
        stdout += data;
      });

      ffprobe.stdout.on('close', function() {
        stdoutClosed = true;
        handleExit();
      });

      ffprobe.stderr.on('data', function(data) {
        stderr += data;
      });

      ffprobe.stderr.on('close', function() {
        stderrClosed = true;
        handleExit();
      });
    });
  };
};

================================================ FILE: doc/fluent-ffmpeg.js.html ================================================ JSDoc: Source: fluent-ffmpeg.js

Source: fluent-ffmpeg.js

/*jshint node:true*/
'use strict';

var path = require('path');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

var utils = require('./utils');
var ARGLISTS = ['_global', '_audio', '_audioFilters', '_video', '_videoFilters', '_sizeFilters', '_complexFilters'];


/**
 * Create an ffmpeg command
 *
 * Can be called with or without the 'new' operator, and the 'input' parameter
 * may be specified as 'options.source' instead (or passed later with the
 * addInput method).
 *
 * @constructor
 * @param {String|ReadableStream} [input] input file path or readable stream
 * @param {Object} [options] command options
 * @param {Object} [options.logger=<no logging>] logger object with 'error', 'warning', 'info' and 'debug' methods
 * @param {Number} [options.niceness=0] ffmpeg process niceness, ignored on Windows
 * @param {Number} [options.priority=0] alias for `niceness`
 * @param {String} [options.presets="fluent-ffmpeg/lib/presets"] directory to load presets from
 * @param {String} [options.preset="fluent-ffmpeg/lib/presets"] alias for `presets`
 * @param {String} [options.stdoutLines=100] maximum lines of ffmpeg output to keep in memory, use 0 for unlimited
 * @param {Number} [options.timeout=<no timeout>] ffmpeg processing timeout in seconds
 * @param {String|ReadableStream} [options.source=<no input>] alias for the `input` parameter
 */
function FfmpegCommand(input, options) {
  // Make 'new' optional
  if (!(this instanceof FfmpegCommand)) {
    return new FfmpegCommand(input, options);
  }

  EventEmitter.call(this);

  if (typeof input === 'object' && !('readable' in input)) {
    // Options object passed directly
    options = input;
  } else {
    // Input passed first
    options = options || {};
    options.source = input;
  }

  // Add input if present
  this._inputs = [];
  if (options.source) {
    this.input(options.source);
  }

  // Add target-less output for backwards compatibility
  this._outputs = [];
  this.output();

  // Create argument lists
  var self = this;
  ['_global', '_complexFilters'].forEach(function(prop) {
    self[prop] = utils.args();
  });

  // Set default option values
  options.stdoutLines = 'stdoutLines' in options ? options.stdoutLines : 100;
  options.presets = options.presets || options.preset || path.join(__dirname, 'presets');
  options.niceness = options.niceness || options.priority || 0;

  // Save options
  this.options = options;

  // Setup logger
  this.logger = options.logger || {
    debug: function() {},
    info: function() {},
    warn: function() {},
    error: function() {}
  };
}
util.inherits(FfmpegCommand, EventEmitter);
module.exports = FfmpegCommand;


/**
 * Clone an ffmpeg command
 *
 * This method is useful when you want to process the same input multiple times.
 * It returns a new FfmpegCommand instance with the exact same options.
 *
 * All options set _after_ the clone() call will only be applied to the instance
 * it has been called on.
 *
 * @example
 *   var command = ffmpeg('/path/to/source.avi')
 *     .audioCodec('libfaac')
 *     .videoCodec('libx264')
 *     .format('mp4');
 *
 *   command.clone()
 *     .size('320x200')
 *     .save('/path/to/output-small.mp4');
 *
 *   command.clone()
 *     .size('640x400')
 *     .save('/path/to/output-medium.mp4');
 *
 *   command.save('/path/to/output-original-size.mp4');
 *
 * @method FfmpegCommand#clone
 * @return FfmpegCommand
 */
FfmpegCommand.prototype.clone = function() {
  var clone = new FfmpegCommand();
  var self = this;

  // Clone options and logger
  clone.options = this.options;
  clone.logger = this.logger;

  // Clone inputs
  clone._inputs = this._inputs.map(function(input) {
    return {
      source: input.source,
      options: input.options.clone()
    };
  });

  // Create first output
  if ('target' in this._outputs[0]) {
    // We have outputs set, don't clone them and create first output
    clone._outputs = [];
    clone.output();
  } else {
    // No outputs set, clone first output options
    clone._outputs = [
      clone._currentOutput = {
        flags: {}
      }
    ];

    ['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) {
      clone._currentOutput[key] = self._currentOutput[key].clone();
    });

    if (this._currentOutput.sizeData) {
      clone._currentOutput.sizeData = {};
      utils.copy(this._currentOutput.sizeData, clone._currentOutput.sizeData);
    }

    utils.copy(this._currentOutput.flags, clone._currentOutput.flags);
  }

  // Clone argument lists
  ['_global', '_complexFilters'].forEach(function(prop) {
    clone[prop] = self[prop].clone();
  });

  return clone;
};


/* Add methods from options submodules */

require('./options/inputs')(FfmpegCommand.prototype);
require('./options/audio')(FfmpegCommand.prototype);
require('./options/video')(FfmpegCommand.prototype);
require('./options/videosize')(FfmpegCommand.prototype);
require('./options/output')(FfmpegCommand.prototype);
require('./options/custom')(FfmpegCommand.prototype);
require('./options/misc')(FfmpegCommand.prototype);


/* Add processor methods */

require('./processor')(FfmpegCommand.prototype);


/* Add capabilities methods */

require('./capabilities')(FfmpegCommand.prototype);

FfmpegCommand.setFfmpegPath = function(path) {
  (new FfmpegCommand()).setFfmpegPath(path);
};

FfmpegCommand.setFfprobePath = function(path) {
  (new FfmpegCommand()).setFfprobePath(path);
};

FfmpegCommand.setFlvtoolPath = function(path) {
  (new FfmpegCommand()).setFlvtoolPath(path);
};

FfmpegCommand.availableFilters =
FfmpegCommand.getAvailableFilters = function(callback) {
  (new FfmpegCommand()).availableFilters(callback);
};

FfmpegCommand.availableCodecs =
FfmpegCommand.getAvailableCodecs = function(callback) {
  (new FfmpegCommand()).availableCodecs(callback);
};

FfmpegCommand.availableFormats =
FfmpegCommand.getAvailableFormats = function(callback) {
  (new FfmpegCommand()).availableFormats(callback);
};


/* Add ffprobe methods */

require('./ffprobe')(FfmpegCommand.prototype);

FfmpegCommand.ffprobe = function(file) {
  var instance = new FfmpegCommand(file);
  instance.ffprobe.apply(instance, Array.prototype.slice.call(arguments, 1));
};

/* Add processing recipes */

require('./recipes')(FfmpegCommand.prototype);

================================================ FILE: doc/global.html ================================================ JSDoc: Global

Global

Methods

<private> createSizeFilters(command, key, value)

Recompute size filters

Parameters:
Name Type Description
command FfmpegCommand
key String

newly-added parameter name ('size', 'aspect' or 'pad')

value String

newly-added parameter value

Source:
Returns:

filter string array

<private> getScalePadFilters(width, height, aspect, color)

Return filters to pad video to width*height,

Parameters:
Name Type Description
width Number

output width

height Number

output height

aspect Number

video aspect ratio (without padding)

color Number

padding color

Source:
Returns:

scale/pad filters

<private> parseProgressLine(line)

Parse progress line from ffmpeg stderr

Parameters:
Name Type Description
line String

progress line

Source:
Returns:

progress object

<private> process(command, target, pipeOptions)

Parameters:
Name Type Argument Description
command FfmpegCommand
target String | Writable
pipeOptions Object <optional>
Source:

<private> runFfprobe(command)

Run ffprobe asynchronously and store data in command

Parameters:
Name Type Description
command FfmpegCommand
Source:

================================================ FILE: doc/index.html ================================================ JSDoc: Index

Index

Fluent ffmpeg-API for node.js

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

This is the documentation for fluent-ffmpeg 2.x. You can still access the code and documentation for fluent-ffmpeg 1.7 here.

Installation

Via npm:

$ npm install fluent-ffmpeg

Or as a submodule:

$ git submodule add git://github.com/schaermu/node-fluent-ffmpeg.git vendor/fluent-ffmpeg

Usage

You will find a lot of usage examples (including a real-time streaming example using flowplayer and express!) in the examples folder.

Prerequisites

ffmpeg and ffprobe

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

flvtool2 or flvmeta

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. If you set either the FLVTOOL2_PATH or FLVMETA_PATH, fluent-ffmpeg will try to use it instead of searching in the PATH.

Setting binary paths manually

Alternatively, you may set the ffmpeg, ffprobe and flvtool2/flvmeta binary paths manually by using the following API commands:

  • Ffmpeg.setFfmpegPath(path) Argument path is a string with the full path to the ffmpeg binary.
  • Ffmpeg.setFfprobePath(path) Argument path is a string with the full path to the ffprobe binary.
  • Ffmpeg.setFlvtoolPath(path) Argument path is a string with the full path to the flvtool2 or flvmeta binary.

Creating an FFmpeg command

The fluent-ffmpeg module returns a constructor that you can use to instanciate FFmpeg commands.

var FfmpegCommand = require('fluent-ffmpeg');
var command = new FfmpegCommand();

You can also use the constructor without the new operator.

var ffmpeg = require('fluent-ffmpeg');
var command = ffmpeg();

You may pass an input file name or readable stream, a configuration object, or both to the constructor.

var command = ffmpeg('/path/to/file.avi');
var command = ffmpeg(fs.createReadStream('/path/to/file.avi'));
var command = ffmpeg({ option: "value", ... });
var command = ffmpeg('/path/to/file.avi', { option: "value", ... });

The following options are available:

  • source: input file name or readable stream (ignored if an input file is passed to the constructor)
  • timeout: ffmpeg timeout in seconds (defaults to no timeout)
  • preset or presets: directory to load module presets from (defaults to the lib/presets directory in fluent-ffmpeg tree)
  • niceness or priority: ffmpeg niceness value, between -20 and 20; ignored on Windows platforms (defaults to 0)
  • logger: logger object with debug(), info(), warn() and error() methods (defaults to no logging)
  • stdoutLines: maximum number of lines from ffmpeg stdout/stderr to keep in memory (defaults to 100, use 0 for unlimited storage)

Specifying inputs

You can add any number of inputs to an Ffmpeg command. An input can be:

  • a file name (eg. /path/to/file.avi);
  • an image pattern (eg. /path/to/frame%03d.png);
  • a readable stream; only one input stream may be used for a command, but you can use both an input stream and one or several file names.
// Note that all fluent-ffmpeg methods are chainable
ffmpeg('/path/to/input1.avi')
  .input('/path/to/input2.avi')
  .input(fs.createReadStream('/path/to/input3.avi'));

// Passing an input to the constructor is the same as calling .input()
ffmpeg()
  .input('/path/to/input1.avi')
  .input('/path/to/input2.avi');

// Most methods have several aliases, here you may use addInput or mergeAdd instead
ffmpeg()
  .addInput('/path/to/frame%02d.png')
  .addInput('/path/to/soundtrack.mp3');

ffmpeg()
  .mergeAdd('/path/to/input1.avi')
  .mergeAdd('/path/to/input2.avi');

Input options

The following methods enable passing input-related options to ffmpeg. Each of these methods apply on the last input added (including the one passed to the constructor, if any). You must add an input before calling those, or an error will be thrown.

inputFormat(format): specify input format

Aliases: fromFormat(), withInputFormat().

This is only useful for raw inputs, as ffmpeg can determine the input format automatically.

ffmpeg()
  .input('/dev/video0')
  .inputFormat('mov')
  .input('/path/to/file.avi')
  .inputFormat('avi');

Fluent-ffmpeg checks for format availability before actually running the command, and throws an error when a specified input format is not available.

inputFPS(fps): specify input framerate

Aliases: withInputFps(), withInputFPS(), withFpsInput(), withFPSInput(), inputFps(), fpsInput(), FPSInput().

This is only valid for raw inputs, as ffmpeg can determine the input framerate automatically.

ffmpeg('/dev/video0').inputFPS(29.7);

native(): read input at native framerate

Aliases: nativeFramerate(), withNativeFramerate().

ffmpeg('/path/to/file.avi').native();

seekInput(time): set input start time

Alias: setStartTime().

Seeks an input and only start decoding at given time offset. The time argument may be a number (in seconds) or a timestamp string (with format [[hh:]mm:]ss[.xxx]).

ffmpeg('/path/to/file.avi').seekInput(134.5);
ffmpeg('/path/to/file.avi').seekInput('2:14.500');

loop([duration]): loop over input

ffmpeg('/path/to/file.avi').loop();
ffmpeg('/path/to/file.avi').loop(134.5);
ffmpeg('/path/to/file.avi').loop('2:14.500');

inputOptions(option...): add custom input options

Aliases: inputOption(), addInputOption(), addInputOptions(), withInputOption(), withInputOptions().

This method allows passing any input-related option to ffmpeg. You can call it with a single argument to pass a single option, optionnaly with a space-separated parameter:

/* Single option */
ffmpeg('/path/to/file.avi').inputOptions('-someOption');

/* Single option with parameter */
ffmpeg('/dev/video0').inputOptions('-r 24');

You may also pass multiple options at once by passing an array to the method:

ffmpeg('/path/to/file.avi').inputOptions([
  '-option1',
  '-option2 param2',
  '-option3',
  '-option4 param4'
]);

Finally, you may also directly pass command line tokens as separate arguments to the method:

ffmpeg('/path/to/file.avi').inputOptions(
  '-option1',
  '-option2', 'param2',
  '-option3',
  '-option4', 'param4'
);

Audio options

The following methods change the audio stream(s) in the produced output.

noAudio(): disable audio altogether

Aliases: withNoAudio().

Disables audio in the output and remove any previously set audio option.

ffmpeg('/path/to/file.avi').noAudio();

audioCodec(codec): set audio codec

Aliases: withAudioCodec().

ffmpeg('/path/to/file.avi').audioCodec('libmp3lame');

Fluent-ffmpeg checks for codec availability before actually running the command, and throws an error when a specified audio codec is not available.

audioBitrate(bitrate): set audio bitrate

Aliases: withAudioBitrate().

Sets the audio bitrate in kbps. The bitrate parameter may be a number or a string with an optional k suffix. This method is used to enforce a constant bitrate; use audioQuality() to encode using a variable bitrate.

ffmpeg('/path/to/file.avi').audioBitrate(128);
ffmpeg('/path/to/file.avi').audioBitrate('128');
ffmpeg('/path/to/file.avi').audioBitrate('128k');

audioChannels(count): set audio channel count

Aliases: withAudioChannels().

ffmpeg('/path/to/file.avi').audioChannels(2);

audioFrequency(freq): set audio frequency

Aliases: withAudioFrequency().

The freq parameter specifies the audio frequency in Hz.

ffmpeg('/path/to/file.avi').audioFrequency(22050);

audioQuality(quality): set audio quality

Aliases: withAudioQuality().

This method fixes a quality factor for the audio codec (VBR encoding). The quality scale depends on the actual codec used.

ffmpeg('/path/to/file.avi')
  .audioCodec('libmp3lame')
  .audioQuality(0);

audioFilters(filter...): add custom audio filters

Aliases: audioFilter(), withAudioFilter(), withAudioFilters().

This method enables adding custom audio filters. You may add multiple filters at once by passing either several arguments or an array. See the Ffmpeg documentation for available filters and their syntax.

Each filter pased to this method can be either a filter string (eg. volume=0.5) or a filter specification object with the following keys:

  • filter: filter name
  • options: optional; either an option string for the filter (eg. n=-50dB:d=5), an options array for unnamed options (eg. ['-50dB', 5]) or an object mapping option names to values (eg. { n: '-50dB', d: 5 }). When options is not specified, the filter will be added without any options.
ffmpeg('/path/to/file.avi')
  .audioFilters('volume=0.5')
  .audioFilters('silencedetect=n=-50dB:d=5');

ffmpeg('/path/to/file.avi')
  .audioFilters('volume=0.5', 'silencedetect=n=-50dB:d=5');

ffmpeg('/path/to/file.avi')
  .audioFilters(['volume=0.5', 'silencedetect=n=-50dB:d=5']);

ffmpeg('/path/to/file.avi')
  .audioFilters([
    {
      filter: 'volume',
      options: '0.5'
    },
    {
      filter: 'silencedetect',
      options: 'n=-50dB:d=5'
    }
  ]);

ffmpeg('/path/to/file.avi')
  .audioFilters(
    {
      filter: 'volume',
      options: ['0.5']
    },
    {
      filter: 'silencedetect',
      options: { n: '-50dB', d: 5 }
    }
  ]);

Video options

The following methods change the video stream(s) in the produced output.

noVideo(): disable video altogether

Aliases: withNoVideo().

This method disables video output and removes any previously set video option.

ffmpeg('/path/to/file.avi').noVideo();

videoCodec(codec): set video codec

Aliases: withVideoCodec().

ffmpeg('/path/to/file.avi').videoCodec('libx264');

Fluent-ffmpeg checks for codec availability before actually running the command, and throws an error when a specified video codec is not available.

videoBitrate(bitrate[, constant=false]): set video bitrate

Aliases: withVideoBitrate().

Sets the target video bitrate in kbps. The bitrate argument may be a number or a string with an optional k suffix. The constant argument specifies whether a constant bitrate should be enforced (defaults to false).

Keep in mind that, depending on the codec used, enforcing a constant bitrate often comes at the cost of quality. The best way to have a constant video bitrate without losing too much quality is to use 2-pass encoding (see Fffmpeg documentation).

ffmpeg('/path/to/file.avi').videoBitrate(1000);
ffmpeg('/path/to/file.avi').videoBitrate('1000');
ffmpeg('/path/to/file.avi').videoBitrate('1000k');
ffmpeg('/path/to/file.avi').videoBitrate('1000k', true);

videoFilters(filter...): add custom video filters

Aliases: videoFilter(), withVideoFilter(), withVideoFilters().

This method enables adding custom video filters. You may add multiple filters at once by passing either several arguments or an array. See the Ffmpeg documentation for available filters and their syntax.

Each filter pased to this method can be either a filter string (eg. fade=in:0:30) or a filter specification object with the following keys:

  • filter: filter name
  • options: optional; either an option string for the filter (eg. in:0:30), an options array for unnamed options (eg. ['in', 0, 30]) or an object mapping option names to values (eg. { t: 'in', s: 0, n: 30 }). When options is not specified, the filter will be added without any options.
ffmpeg('/path/to/file.avi')
  .videoFilters('fade=in:0:30')
  .videoFilters('pad=640:480:0:40:violet');

ffmpeg('/path/to/file.avi')
  .videoFilters('fade=in:0:30', 'pad=640:480:0:40:violet');

ffmpeg('/path/to/file.avi')
  .videoFilters(['fade=in:0:30', 'pad=640:480:0:40:violet']);

ffmpeg('/path/to/file.avi')
  .videoFilters([
    {
      filter: 'fade',
      options: 'in:0:30'
    },
    {
      filter: 'pad',
      options: '640:480:0:40:violet'
    }
  ]);

ffmpeg('/path/to/file.avi')
    .videoFilters(
    {
      filter: 'fade',
      options: ['in', 0, 30]
    },
    {
      filter: 'filter2',
      options: { w: 640, h: 480, x: 0, y: 40, color: 'violet' }
    }
  );

fps(fps): set output framerate

Aliases: withOutputFps(), withOutputFPS(), withFpsOutput(), withFPSOutput(), withFps(), withFPS(), outputFPS(), outputFps(), fpsOutput(), FPSOutput(), FPS().

ffmpeg('/path/to/file.avi').fps(29.7);

frames(count): specify frame count

Aliases: takeFrames(), withFrames().

Set ffmpeg to only encode a certain number of frames.

ffmpeg('/path/to/file.avi').frames(240);

Video frame size options

The following methods enable resizing the output video frame size. They all work together to generate the appropriate video filters.

size(size): set output frame size

Aliases: videoSize(), withSize().

This method sets the output frame size. The size argument may have one of the following formats:

  • 640x480: set a fixed output frame size. Unless autopad() is called, this may result in the video being stretched or squeezed to fit the requested size.
  • 640x?: set a fixed width and compute height automatically. If aspect() is also called, it is used to compute video height; otherwise it is computed so that the input aspect ratio is preserved.
  • ?x480: set a fixed height and compute width automatically. If aspect() is also called, it is used to compute video width; otherwise it is computed so that the input aspect ratio is preserved.
  • 50%: rescale both width and height to the given percentage. Aspect ratio is always preserved.

Note that for compatibility with some codecs, computed dimensions are always rounded down to multiples of 2.

ffmpeg('/path/to/file.avi').size('640x480');
ffmpeg('/path/to/file.avi').size('640x?');
ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3');
ffmpeg('/path/to/file.avi').size('50%');

aspect(aspect): set output frame aspect ratio

Aliases: withAspect(), withAspectRatio(), setAspect(), setAspectRatio(), aspectRatio().

This method enforces a specific output aspect ratio. The aspect argument may either be a number or a X:Y string.

Note that calls to aspect() are ignored when size() has been called with a fixed width and height or a percentage, and also when size() has not been called at all.

ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3');
ffmpeg('/path/to/file.avi').size('640x?').aspect(1.33333);

autopad([color='black']): enable auto-padding the output video

Aliases: applyAutopadding(), applyAutoPadding(), applyAutopad(), applyAutoPad(), withAutopadding(), withAutoPadding(), withAutopad(), withAutoPad(), autoPad().

This method enables applying auto-padding to the output video. The color parameter specifies which color to use for padding, and must be a color code or name supported by ffmpeg (defaults to 'black').

The behaviour of this method depends on calls made to other video size methods:

  • when size() has been called with a percentage or has not been called, it is ignored;
  • when size() has been called with WxH, it adds padding so that the input aspect ratio is kept;
  • when size() has been called with either Wx? or ?xH, padding is only added if aspect() was called (otherwise the output dimensions are computed from the input aspect ratio and padding is not needed).
// No size specified, autopad() is ignored
ffmpeg('/path/to/file.avi').autopad();

// Adds padding to keep original aspect ratio.
// - with a 640x400 input, 40 pixels of padding are added on both sides
// - with a 600x480 input, 20 pixels of padding are added on top and bottom
// - with a 320x200 input, video is scaled up to 640x400 and 40px of padding
//   is added on both sides
// - with a 320x240 input, video is scaled up to 640x480 and and no padding
//   is needed
ffmpeg('/path/to/file.avi').size('640x480').autopad();
ffmpeg('/path/to/file.avi').size('640x480').autopad('white');
ffmpeg('/path/to/file.avi').size('640x480').autopad('#35A5FF');

// Size computed from input, autopad() is ignored
ffmpeg('/path/to/file.avi').size('50%').autopad();
ffmpeg('/path/to/file.avi').size('640x?').autopad();
ffmpeg('/path/to/file.avi').size('?x480').autopad();

// Calling .size('640x?').aspect('4:3') is similar to calling .size('640x480')
// - with a 640x400 input, 40 pixels of padding are added on both sides
// - with a 600x480 input, 20 pixels of padding are added on top and bottom
// - with a 320x200 input, video is scaled up to 640x400 and 40px of padding
//   is added on both sides
// - with a 320x240 input, video is scaled up to 640x480 and and no padding
//   is needed
ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad();
ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad('white');
ffmpeg('/path/to/file.avi').size('640x?').aspect('4:3').autopad('#35A5FF');

// Calling .size('?x480').aspect('4:3') is similar to calling .size('640x480')
ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad();
ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad('white');
ffmpeg('/path/to/file.avi').size('?x480').aspect('4:3').autopad('#35A5FF');

For compatibility with previous fluent-ffmpeg versions, this method also accepts an additional boolean first argument, which specifies whether to apply auto-padding.

ffmpeg('/path/to/file.avi').size('640x480').autopad(true);
ffmpeg('/path/to/file.avi').size('640x480').autopad(true, 'pink');

keepDAR(): force keeping display aspect ratio

Aliases: keepPixelAspect(), keepDisplayAspect(), keepDisplayAspectRatio().

This method is useful when converting an input with non-square pixels to an output format that does not support non-square pixels (eg. most image formats). It rescales the input so that the display aspect ratio is the same.

ffmpeg('/path/to/file.avi').keepDAR();

Specifying multiple outputs

output(target[, options]): add an output to the command

Aliases: addOutput().

Adds an output to the command. The target argument may be an output filename or a writable stream (but at most one output stream may be used with a single command).

When target is a stream, an additional options object may be passed. If it is present, it will be passed ffmpeg output stream pipe() method.

Adding an output switches the "current output" of the command, so that any fluent-ffmpeg method that applies to an output is indeed applied to the last output added. For backwards compatibility reasons, you may as well call those methods before adding the first output (in which case they will apply to the first output when it is added). Methods that apply to an output are all non-input-related methods, except for complexFilter(), which is global.

Also note that when calling output(), you should not use the save() or stream() (formerly saveToFile() and writeToStream()) methods, as they already add an output. Use the run() method to start processing.

var stream  = fs.createWriteStream('outputfile.divx');

ffmpeg('/path/to/file.avi')
  .output('outputfile.mp4')
  .output(stream);

ffmpeg('/path/to/file.avi')
  // You may pass a pipe() options object when using a stream
  .output(stream, { end:true });

// Output-related methods apply to the last output added
ffmpeg('/path/to/file.avi')

  .output('outputfile.mp4')
  .audioCodec('libfaac')
  .videoCodec('libx264')
  .size('320x200')

  .output(stream)
  .preset('divx')
  .size('640x480');

// Use the run() method to run commands with multiple outputs
ffmpeg('/path/to/file.avi')
  .output('outputfile.mp4')
  .output(stream)
  .on('end', function() {
    console.log('Finished processing');
  })
  .run();

Output options

duration(time): set output duration

Aliases: withDuration(), setDuration().

Forces ffmpeg to stop transcoding after a specific output duration. The time parameter may be a number (in seconds) or a timestamp string (with format [[hh:]mm:]ss[.xxx]).

ffmpeg('/path/to/file.avi').duration(134.5);
ffmpeg('/path/to/file.avi').duration('2:14.500');

seek(time): seek output

Aliases: seekOutput().

Seeks streams before encoding them into the output. This is different from calling seekInput() in that the offset will only apply to one output. This is also slower, as skipped frames will still be decoded (but dropped).

The time argument may be a number (in seconds) or a timestamp string (with format [[hh:]mm:]ss[.xxx]).

ffmpeg('/path/to/file.avi')
  .seekInput('1:00')

  .output('from-1m30s.avi')
  .seek(30)

  .output('from-1m40s.avi')
  .seek('0:40');

format(format): set output format

Aliases: withOutputFormat(), toFormat(), outputFormat().

ffmpeg('/path/to/file.avi').format('flv');

flvmeta(): update FLV metadata after transcoding

Aliases: updateFlvMetadata().

Calling this method makes fluent-ffmpeg run flvmeta or flvtool2 on the output file to add FLV metadata and make files streamable. It does not work when outputting to a stream, and is only useful when outputting to FLV format.

ffmpeg('/path/to/file.avi').flvmeta().format('flv');

outputOptions(option...): add custom output options

Aliases: outputOption(), addOutputOption(), addOutputOptions(), withOutputOption(), withOutputOptions(), addOption(), addOptions().

This method allows passing any output-related option to ffmpeg. You can call it with a single argument to pass a single option, optionnaly with a space-separated parameter:

/* Single option */
ffmpeg('/path/to/file.avi').outputOptions('-someOption');

/* Single option with parameter */
ffmpeg('/dev/video0').outputOptions('-r 24');

You may also pass multiple options at once by passing an array to the method:

ffmpeg('/path/to/file.avi').outputOptions([
  '-option1',
  '-option2 param2',
  '-option3',
  '-option4 param4'
]);

Finally, you may also directly pass command line tokens as separate arguments to the method:

ffmpeg('/path/to/file.avi').outputOptions(
  '-option1',
  '-option2', 'param2',
  '-option3',
  '-option4', 'param4'
);

Miscellaneous options

preset(preset): use fluent-ffmpeg preset

Aliases: usingPreset().

There are two kinds of presets supported by fluent-ffmpeg. The first one is preset modules; to use those, pass the preset name as the preset argument. Preset modules are loaded from the directory specified by the presets constructor option (defaults to the lib/presets fluent-ffmpeg subdirectory).

// Uses <path-to-fluent-ffmpeg>/lib/presets/divx.js
ffmpeg('/path/to/file.avi').preset('divx');

// Uses /my/presets/foo.js
ffmpeg('/path/to/file.avi', { presets: '/my/presets' }).preset('foo');

Preset modules must export a load() function that takes an FfmpegCommand as an argument. fluent-ffmpeg comes with the following preset modules preinstalled:

  • divx
  • flashvideo
  • podcast

Here is the code from the included divx preset as an example:

exports.load = function(ffmpeg) {
  ffmpeg
    .format('avi')
    .videoBitrate('1024k')
    .videoCodec('mpeg4')
    .size('720x?')
    .audioBitrate('128k')
    .audioChannels(2)
    .audioCodec('libmp3lame')
    .outputOptions(['-vtag DIVX']);
};

The second kind of preset is preset functions. To use those, pass a function which takes an FfmpegCommand as a parameter.

function myPreset(command) {
  command.format('avi').size('720x?');
}

ffmpeg('/path/to/file.avi').preset(myPreset);

complexFilter(filters[, map]): set complex filtergraph

Aliases: filterGraph()

The complexFilter() method enables setting a complex filtergraph for a command. It expects a filter specification (or a filter specification array) and an optional output mapping parameter as arguments.

Filter specifications may be either plain ffmpeg filter strings (eg. split=3[a][b][c]) or objects with the following keys:

  • filter: filter name
  • options: optional; either an option string for the filter (eg. in:0:30), an options array for unnamed options (eg. ['in', 0, 30]) or an object mapping option names to values (eg. { t: 'in', s: 0, n: 30 }). When options is not specified, the filter will be added without any options.
  • inputs: optional; input stream specifier(s) for the filter. The value may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets. When input streams are not specified, ffmpeg will use the first unused streams of the correct type.
  • outputs: optional; output stream specifier(s) for the filter. The value may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets.

The output mapping parameter specifies which stream(s) to include in the output from the filtergraph. It may be either a single stream specifier string or an array of stream specifiers. Each specifier can be optionally enclosed in square brackets. When this parameter is not present, ffmpeg will default to saving all unused outputs to the output file.

Note that only one complex filtergraph may be set on a given command. Calling complexFilter() again will override any previously set filtergraph, but you can set as many filters as needed in a single call.

ffmpeg('/path/to/file.avi')
  .complexFilter([
    // Rescale input stream into stream 'rescaled'
    'scale=640:480[rescaled]',

    // Duplicate rescaled stream 3 times into streams a, b, and c
    {
      filter: 'split', options: '3',
      inputs: 'rescaled', outputs: ['a', 'b', 'c']
    },

    // Create stream 'red' by removing green and blue channels from stream 'a'
    {
      filter: 'lutrgb', options: { g: 0, b: 0 },
      inputs: 'a', outputs: 'red'
    },

    // Create stream 'green' by removing red and blue channels from stream 'b'
    {
      filter: 'lutrgb', options: { r: 0, b: 0 },
      inputs: 'b', outputs: 'green'
    },

    // Create stream 'blue' by removing red and green channels from stream 'c'
    {
      filter: 'lutrgb', options: { r: 0, g: 0 },
      inputs: 'c', outputs: 'blue'
    },

    // Pad stream 'red' to 3x width, keeping the video on the left,
    // and name output 'padded'
    {
      filter: 'pad', options: { w: 'iw*3', h: 'ih' },
      inputs: 'red', outputs: 'padded'
    },

    // Overlay 'green' onto 'padded', moving it to the center,
    // and name output 'redgreen'
    {
      filter: 'overlay', options: { x: 'w', y: 0 },
      inputs: ['padded', 'green'], outputs: 'redgreen'
    },

    // Overlay 'blue' onto 'redgreen', moving it to the right
    {
      filter: 'overlay', options: { x: '2*w', y: 0 },
      inputs: ['redgreen', 'blue'], outputs: 'output'
    },
  ], 'output');

Setting event handlers

Before actually running a command, you may want to set event listeners on it to be notified when it's done. The following events are available:

'start': ffmpeg process started

The start event is emitted just after ffmpeg has been spawned. It is emitted with the full command line used as an argument.

ffmpeg('/path/to/file.avi')
  .on('start', function(commandLine) {
    console.log('Spawned Ffmpeg with command: ' + commandLine);
  });

'codecData': input codec data available

The codecData event is emitted when ffmpeg outputs codec information about its input streams. It is emitted with an object argument with the following keys:

  • format: input format
  • duration: input duration
  • audio: audio codec
  • audio_details: audio encoding details
  • video: video codec
  • video_details: video encoding details
ffmpeg('/path/to/file.avi')
  .on('codecData', function(data) {
    console.log('Input is ' + data.audio + ' audio ' +
      'with ' + data.video + ' video');
  });

'progress': transcoding progress information

The progress event is emitted every time ffmpeg reports progress information. It is emitted with an object argument with the following keys:

  • frames: total processed frame count
  • currentFps: framerate at which FFmpeg is currently processing
  • currentKbps: throughput at which FFmpeg is currently processing
  • targetSize: current size of the target file in kilobytes
  • timemark: the timestamp of the current frame in seconds
  • percent: an estimation of the progress percentage

Note that percent can be (very) inaccurate, as the only progress information fluent-ffmpeg gets from ffmpeg is the total number of frames written (and the corresponding duration). To estimate percentage, fluent-ffmpeg has to guess what the total output duration will be, and uses the first input added to the command to do so. In particular:

  • percentage is not available when using an input stream
  • percentage may be wrong when using multiple inputs with different durations and the first one is not the longest
ffmpeg('/path/to/file.avi')
  .on('progress', function(progress) {
    console.log('Processing: ' + progress.percent + '% done');
  });

'stderr': FFmpeg output

The stderr event is emitted every time FFmpeg outputs a line to stderr. It is emitted with a string containing the line of stderr (minus trailing new line characters).

ffmpeg('/path/to/file.avi')
  .on('stderr', function(stderrLine) {
    console.log('Stderr output: ' + stderrLine);
  });

'error': transcoding error

The error event is emitted when an error occurs when running ffmpeg or when preparing its execution. It is emitted with an error object as an argument. If the error happened during ffmpeg execution, listeners will also receive two additional arguments containing ffmpegs stdout and stderr.

Warning: you should always set a handler for the error event, as node's default behaviour when an error event without any listeners is emitted is to output the error to the console and terminate the program.

ffmpeg('/path/to/file.avi')
  .on('error', function(err, stdout, stderr) {
    console.log('Cannot process video: ' + err.message);
  });

'end': processing finished

The end event is emitted when processing has finished. Listeners receive ffmpeg standard output and standard error as arguments, except when generating thumbnails (see below), in which case they receive an array of the generated filenames.

ffmpeg('/path/to/file.avi')
  .on('end', function(stdout, stderr) {
    console.log('Transcoding succeeded !');
  });

stdout is empty when the command outputs to a stream. Both stdout and stderr are limited by the stdoutLines option (defaults to 100 lines).

Starting FFmpeg processing

save(filename): save the output to a file

Aliases: saveToFile()

Starts ffmpeg processing and saves the output to a file.

ffmpeg('/path/to/file.avi')
  .videoCodec('libx264')
  .audioCodec('libmp3lame')
  .size('320x240')
  .on('error', function(err) {
    console.log('An error occurred: ' + err.message);
  })
  .on('end', function() {
    console.log('Processing finished !');
  })
  .save('/path/to/output.mp4');

Note: the save() method is actually syntactic sugar for calling both output() and run().

pipe([stream], [options]): pipe the output to a writable stream

Aliases: stream(), writeToStream().

Starts processing and pipes ffmpeg output to a writable stream. The options argument, if present, is passed to ffmpeg output stream's pipe() method (see nodejs documentation).

var outStream = fs.createWriteStream('/path/to/output.mp4');

ffmpeg('/path/to/file.avi')
  .videoCodec('libx264')
  .audioCodec('libmp3lame')
  .size('320x240')
  .on('error', function(err) {
    console.log('An error occurred: ' + err.message);
  })
  .on('end', function() {
    console.log('Processing finished !');
  })
  .pipe(outStream, { end: true });

When no stream argument is present, the pipe() method returns a PassThrough stream, which you can pipe to somewhere else (or just listen to events on).

Note: this is only available with node >= 0.10.

var command = ffmpeg('/path/to/file.avi')
  .videoCodec('libx264')
  .audioCodec('libmp3lame')
  .size('320x240')
  .on('error', function(err) {
    console.log('An error occurred: ' + err.message);
  })
  .on('end', function() {
    console.log('Processing finished !');
  });

var ffstream = command.pipe();
ffstream.on('data', function(chunk) {
  console.log('ffmpeg just wrote ' + chunk.length + ' bytes');
});

Note: the stream() method is actually syntactic sugar for calling both output() and run().

run(): start processing

Aliases: exec(), execute().

This method is mainly useful when producing multiple outputs (otherwise the save() or stream() methods are more straightforward). It starts processing with the specified outputs.

Warning: do not use run() when calling other processing methods (eg. save(), pipe() or screenshots()).

ffmpeg('/path/to/file.avi')
  .output('screenshot.png')
  .noAudio()
  .seek('3:00')

  .output('small.avi')
  .audioCodec('copy')
  .size('320x200')

  .output('big.avi')
  .audioCodec('copy')
  .size('640x480')

  .on('error', function(err) {
    console.log('An error occurred: ' + err.message);
  })
  .on('end', function() {
    console.log('Processing finished !');
  })
  .run();

mergeToFile(filename, tmpdir): concatenate multiple inputs

Use the input 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.

ffmpeg('/path/to/part1.avi')
  .input('/path/to/part2.avi')
  .input('/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');

screenshots(options[, dirname]): generate thumbnails

Aliases: thumbnail(), thumbnails(), screenshot(), takeScreenshots().

Use the screenshots method to extract one or several thumbnails and save them as PNG files. There are a few caveats with this implementation, though:

  • It will not work on input streams.
  • Progress information reported by the progress event is not accurate.
  • It doesn't interract well with filters. In particular, don't use the size() method to resize thumbnails, use the size option instead.

The options argument is an object with the following keys:

  • folder: output folder for generated image files. Defaults to the current folder.
  • filename: output filename pattern (see below). Defaults to "tn.png".
  • count: specifies how many thumbnails to generate. When using this option, thumbnails are generated at regular intervals in the video (for example, when requesting 3 thumbnails, at 25%, 50% and 75% of the video length). count is ignored when timemarks or timestamps is specified.
  • timemarks or timestamps: specifies an array of timestamps in the video where thumbnails should be taken. Each timestamp may be a number (in seconds), a percentage string (eg. "50%") or a timestamp string with format "hh:mm:ss.xxx" (where hours, minutes and milliseconds are both optional).
  • size: specifies a target size for thumbnails (with the same format as the .size() method). Note: you should not use the .size() method when generating thumbnails.

The filename option specifies a filename pattern for generated files. It may contain the following format tokens:

  • '%s': offset in seconds
  • '%w': screenshot width
  • '%h': screenshot height
  • '%r': screenshot resolution (same as '%wx%h')
  • '%f': input filename
  • '%b': input basename (filename w/o extension)
  • '%i': index of screenshot in timemark array (can be zero-padded by using it like %000i)

If multiple timemarks are passed and no variable format token ('%s' or '%i') is specified in the filename pattern, _%i will be added automatically.

When generating thumbnails, an additional filenames event is dispatched with an array of generated filenames as an argument.

ffmpeg('/path/to/video.avi')
  .on('filenames', function(filenames) {
    console.log('Will generate ' + filenames.join(', '))
  })
  .on('end', function() {
    console.log('Screenshots taken');
  })
  .screenshots({
    // Will take screens at 20%, 40%, 60% and 80% of the video
    count: 4,
    folder: '/path/to/output'
  });

ffmpeg('/path/to/video.avi')
  .screenshots({
    timestamps: [30.5, '50%', '01:10.123'],
    filename: 'thumbnail-at-%s-seconds.png',
    folder: '/path/to/output',
    size: '320x240'
  });

Controlling the FFmpeg process

kill([signal='SIGKILL']): kill any running ffmpeg process

This method sends signal (defaults to 'SIGKILL') to the ffmpeg process. It only has sense when processing has started. Sending a signal that terminates the process will result in the error event being emitted.

var command = ffmpeg('/path/to/video.avi')
  .videoCodec('libx264')
  .audioCodec('libmp3lame')
  .on('start', function() {
    // Send SIGSTOP to suspend ffmpeg
    command.kill('SIGSTOP');

    doSomething(function() {
      // Send SIGCONT to resume ffmpeg
      command.kill('SIGCONT');
    });
  })
  .save('/path/to/output.mp4');

// Kill ffmpeg after 60 seconds anyway
setTimeout(function() {
  command.on('error', function() {
    console.log('Ffmpeg has been killed');
  });

  command.kill();
}, 60000);

renice([niceness=0]): change ffmpeg process priority

This method alters the niceness (priority) value of any running ffmpeg process (if any) and any process spawned in the future. The niceness parameter may range from -20 (highest priority) to 20 (lowest priority) and defaults to 0 (which is the default process niceness on most *nix systems).

Note: this method is ineffective on Windows platforms.

// Set startup niceness
var command = ffmpeg('/path/to/file.avi')
  .renice(5)
  .save('/path/to/output.mp4');

// Command takes too long, raise its priority
setTimeout(function() {
  command.renice(-5);
}, 60000);

Reading video metadata

You can read metadata from any valid ffmpeg input file with the modules ffprobe method.

ffmpeg.ffprobe('/path/to/file.avi', function(err, metadata) {
    console.dir(metadata);
});

You may also call the ffprobe method on an FfmpegCommand to probe one of its input. You may pass a 0-based input number as a first argument to specify which input to read metadata from, otherwise the method will probe the last added input.

ffmpeg('/path/to/file1.avi')
  .input('/path/to/file2.avi')
  .ffprobe(function(err, data) {
    console.log('file2 metadata:');
    console.dir(data);
  });

ffmpeg('/path/to/file1.avi')
  .input('/path/to/file2.avi')
  .ffprobe(0, function(err, data) {
    console.log('file1 metadata:');
    console.dir(data);
  });

Warning: ffprobe may be called with an input stream, but in this case it will consume data from the stream, and this data will no longer be available for ffmpeg. Using both ffprobe and a transcoding command on the same input stream will most likely fail unless the stream is a live stream. Only do this if you know what you're doing.

The returned object is the same that is returned by running the following command from your shell (depending on your ffmpeg version you may have to replace -of with -print_format) :

$ 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"
    }
  }
}

Querying ffmpeg capabilities

fluent-ffmpeg enables you to query your installed ffmpeg version for supported formats, codecs, encoders 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.getAvailableEncoders(function(err, encoders) {
  console.log('Available encoders:');
  console.dir(encoders);
});

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 format
  • canMux 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 codec
  • canEncode 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 purposes
  • intraFrameOnly tells if codec can only work with I-frames
  • isLossy tells if codec can do lossy encoding/decoding
  • isLossless 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 encoders looks like:

{
  ...
  libmp3lame: {
    type: 'audio',
    description: 'MP3 (MPEG audio layer 3) (codec mp3)',
    frameMT: false,
    sliceMT: false,
    experimental: false,
    drawHorizBand: false,
    directRendering: false
  },
  ...
}
  • type indicates the encoder type, either "audio", "video" or "subtitle"
  • experimental indicates whether the encoder is experimental. When using such a codec, fluent-ffmpeg automatically adds the '-strict experimental' flag.

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 nothing
  • multipleInputs tells whether the filter can accept multiple inputs
  • output 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

Cloning an FfmpegCommand

You can create clones of an FfmpegCommand instance by calling the clone() method. The clone will be an exact copy of the original at the time it has been called (same inputs, same options, same event handlers, etc.). This is mainly useful when you want to apply different processing options on the same input.

Setting options, adding inputs or event handlers on a clone will not affect the original command.

// Create a command to convert source.avi to MP4
var command = ffmpeg('/path/to/source.avi')
  .audioCodec('libfaac')
  .videoCodec('libx264')
  .format('mp4');

// Create a clone to save a small resized version
command.clone()
  .size('320x200')
  .save('/path/to/output-small.mp4');

// Create a clone to save a medium resized version
command.clone()
  .size('640x400')
  .save('/path/to/output-medium.mp4');

// Save a converted version with the original size
command.save('/path/to/output-original-size.mp4');

Contributing

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!

Code contributions

If you want to add new features or change the API, please submit an issue first to make sure no one else is already working on the same thing and discuss the implementation and API details with maintainers and users by creating an issue. When everything is settled down, you can submit a pull request.

When fixing bugs, you can directly submit a pull request.

Make sure to add tests for your features and bugfixes and update the documentation (see below) before submitting your code!

Documentation contributions

You can directly submit pull requests for documentation changes. Make sure to regenerate the documentation before submitting (see below).

Updating the documentation

When contributing API changes (new methods for example), be sure to update the README file and JSDoc comments in the code. fluent-ffmpeg comes with a plugin that enables two additional JSDoc tags:

  • @aliases: document method aliases
/**
 * ...
 * @method FfmpegCommand#myMethod
 * @aliases myMethodAlias,myOtherMethodAlias
 */
  • @category: set method category
/**
 * ...
 * @category Audio
 */

You can regenerate the JSDoc documentation by running the following command:

$ make doc

To avoid polluting the commit history, make sure to only commit the regenerated JSDoc once and in a specific commit.

Running tests

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.

Main contributors

License

(The MIT License)

Copyright (c) 2011 Stefan Schaermeli <schaermu@gmail.com>

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.


================================================ FILE: doc/inputs.js.html ================================================ JSDoc: Source: options/inputs.js

Source: options/inputs.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');

/*
 *! Input-related methods
 */

module.exports = function(proto) {
  /**
   * Add an input to command
   *
   * Also switches "current input", that is the input that will be affected
   * by subsequent input-related methods.
   *
   * Note: only one stream input is supported for now.
   *
   * @method FfmpegCommand#input
   * @category Input
   * @aliases mergeAdd,addInput
   *
   * @param {String|Readable} source input file path or readable stream
   * @return FfmpegCommand
   */
  proto.mergeAdd =
  proto.addInput =
  proto.input = function(source) {
    var isFile = false;

    if (typeof source !== 'string') {
      if (!('readable' in source) || !(source.readable)) {
        throw new Error('Invalid input');
      }

      var hasInputStream = this._inputs.some(function(input) {
        return typeof input.source !== 'string';
      });

      if (hasInputStream) {
        throw new Error('Only one input stream is supported');
      }

      source.pause();
    } else {
      var protocol = source.match(/^([a-z]{2,}):/i);
      isFile = !protocol || protocol[0] === 'file';
    }

    this._inputs.push(this._currentInput = {
      source: source,
      isFile: isFile,
      options: utils.args()
    });

    return this;
  };


  /**
   * Specify input format for the last specified input
   *
   * @method FfmpegCommand#inputFormat
   * @category Input
   * @aliases withInputFormat,fromFormat
   *
   * @param {String} format input format
   * @return FfmpegCommand
   */
  proto.withInputFormat =
  proto.inputFormat =
  proto.fromFormat = function(format) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-f', format);
    return this;
  };


  /**
   * Specify input FPS for the last specified input
   * (only valid for raw video formats)
   *
   * @method FfmpegCommand#inputFps
   * @category Input
   * @aliases withInputFps,withInputFPS,withFpsInput,withFPSInput,inputFPS,inputFps,fpsInput
   *
   * @param {Number} fps input FPS
   * @return FfmpegCommand
   */
  proto.withInputFps =
  proto.withInputFPS =
  proto.withFpsInput =
  proto.withFPSInput =
  proto.inputFPS =
  proto.inputFps =
  proto.fpsInput =
  proto.FPSInput = function(fps) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-r', fps);
    return this;
  };


  /**
   * Use native framerate for the last specified input
   *
   * @method FfmpegCommand#native
   * @category Input
   * @aliases nativeFramerate,withNativeFramerate
   *
   * @return FfmmegCommand
   */
  proto.nativeFramerate =
  proto.withNativeFramerate =
  proto.native = function() {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-re');
    return this;
  };


  /**
   * Specify input seek time for the last specified input
   *
   * @method FfmpegCommand#seekInput
   * @category Input
   * @aliases setStartTime,seekTo
   *
   * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.setStartTime =
  proto.seekInput = function(seek) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-ss', seek);

    return this;
  };


  /**
   * Loop over the last specified input
   *
   * @method FfmpegCommand#loop
   * @category Input
   *
   * @param {String|Number} [duration] loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.loop = function(duration) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-loop', '1');

    if (typeof duration !== 'undefined') {
      this.duration(duration);
    }

    return this;
  };
};

================================================ FILE: doc/misc.js.html ================================================ JSDoc: Source: options/misc.js

Source: options/misc.js

/*jshint node:true*/
'use strict';

var path = require('path');

/*
 *! Miscellaneous methods
 */

module.exports = function(proto) {
  /**
   * Use preset
   *
   * @method FfmpegCommand#preset
   * @category Miscellaneous
   * @aliases usingPreset
   *
   * @param {String|Function} preset preset name or preset function
   */
  proto.usingPreset =
  proto.preset = function(preset) {
    if (typeof preset === 'function') {
      preset(this);
    } else {
      try {
        var modulePath = path.join(this.options.presets, preset);
        var module = require(modulePath);

        if (typeof module.load === 'function') {
          module.load(this);
        } else {
          throw new Error('preset ' + modulePath + ' has no load() function');
        }
      } catch (err) {
        throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message);
      }
    }

    return this;
  };
};

================================================ FILE: doc/options_audio.js.html ================================================ JSDoc: Source: options/audio.js

Source: options/audio.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Audio-related methods
 */

module.exports = function(proto) {
  /**
   * Disable audio in the output
   *
   * @method FfmpegCommand#noAudio
   * @category Audio
   * @aliases withNoAudio
   * @return FfmpegCommand
   */
  proto.withNoAudio =
  proto.noAudio = function() {
    this._currentOutput.audio.clear();
    this._currentOutput.audioFilters.clear();
    this._currentOutput.audio('-an');

    return this;
  };


  /**
   * Specify audio codec
   *
   * @method FfmpegCommand#audioCodec
   * @category Audio
   * @aliases withAudioCodec
   *
   * @param {String} codec audio codec name
   * @return FfmpegCommand
   */
  proto.withAudioCodec =
  proto.audioCodec = function(codec) {
    this._currentOutput.audio('-acodec', codec);

    return this;
  };


  /**
   * Specify audio bitrate
   *
   * @method FfmpegCommand#audioBitrate
   * @category Audio
   * @aliases withAudioBitrate
   *
   * @param {String|Number} bitrate audio bitrate in kbps (with an optional 'k' suffix)
   * @return FfmpegCommand
   */
  proto.withAudioBitrate =
  proto.audioBitrate = function(bitrate) {
    this._currentOutput.audio('-b:a', ('' + bitrate).replace(/k?$/, 'k'));
    return this;
  };


  /**
   * Specify audio channel count
   *
   * @method FfmpegCommand#audioChannels
   * @category Audio
   * @aliases withAudioChannels
   *
   * @param {Number} channels channel count
   * @return FfmpegCommand
   */
  proto.withAudioChannels =
  proto.audioChannels = function(channels) {
    this._currentOutput.audio('-ac', channels);
    return this;
  };


  /**
   * Specify audio frequency
   *
   * @method FfmpegCommand#audioFrequency
   * @category Audio
   * @aliases withAudioFrequency
   *
   * @param {Number} freq audio frequency in Hz
   * @return FfmpegCommand
   */
  proto.withAudioFrequency =
  proto.audioFrequency = function(freq) {
    this._currentOutput.audio('-ar', freq);
    return this;
  };


  /**
   * Specify audio quality
   *
   * @method FfmpegCommand#audioQuality
   * @category Audio
   * @aliases withAudioQuality
   *
   * @param {Number} quality audio quality factor
   * @return FfmpegCommand
   */
  proto.withAudioQuality =
  proto.audioQuality = function(quality) {
    this._currentOutput.audio('-aq', quality);
    return this;
  };


  /**
   * Specify custom audio filter(s)
   *
   * Can be called both with one or many filters, or a filter array.
   *
   * @example
   * command.audioFilters('filter1');
   *
   * @example
   * command.audioFilters('filter1', 'filter2=param1=value1:param2=value2');
   *
   * @example
   * command.audioFilters(['filter1', 'filter2']);
   *
   * @example
   * command.audioFilters([
   *   {
   *     filter: 'filter1'
   *   },
   *   {
   *     filter: 'filter2',
   *     options: 'param=value:param=value'
   *   }
   * ]);
   *
   * @example
   * command.audioFilters(
   *   {
   *     filter: 'filter1',
   *     options: ['value1', 'value2']
   *   },
   *   {
   *     filter: 'filter2',
   *     options: { param1: 'value1', param2: 'value2' }
   *   }
   * );
   *
   * @method FfmpegCommand#audioFilters
   * @aliases withAudioFilter,withAudioFilters,audioFilter
   * @category Audio
   *
   * @param {...String|String[]|Object[]} filters audio filter strings, string array or
   *   filter specification array, each with the following properties:
   * @param {String} filters.filter filter name
   * @param {String|String[]|Object} [filters.options] filter option string, array, or object
   * @return FfmpegCommand
   */
  proto.withAudioFilter =
  proto.withAudioFilters =
  proto.audioFilter =
  proto.audioFilters = function(filters) {
    if (arguments.length > 1) {
      filters = [].slice.call(arguments);
    }

    if (!Array.isArray(filters)) {
      filters = [filters];
    }

    this._currentOutput.audioFilters(utils.makeFilterStrings(filters));
    return this;
  };
};

================================================ FILE: doc/options_custom.js.html ================================================ JSDoc: Source: options/custom.js

Source: options/custom.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Custom options methods
 */

module.exports = function(proto) {
  /**
   * Add custom input option(s)
   *
   * When passing a single string or an array, each string containing two
   * words is split (eg. inputOptions('-option value') is supported) for
   * compatibility reasons.  This is not the case when passing more than
   * one argument.
   *
   * @example
   * command.inputOptions('option1');
   *
   * @example
   * command.inputOptions('option1', 'option2');
   *
   * @example
   * command.inputOptions(['option1', 'option2']);
   *
   * @method FfmpegCommand#inputOptions
   * @category Custom options
   * @aliases addInputOption,addInputOptions,withInputOption,withInputOptions,inputOption
   *
   * @param {...String} options option string(s) or string array
   * @return FfmpegCommand
   */
  proto.addInputOption =
  proto.addInputOptions =
  proto.withInputOption =
  proto.withInputOptions =
  proto.inputOption =
  proto.inputOptions = function(options) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    var doSplit = true;

    if (arguments.length > 1) {
      options = [].slice.call(arguments);
      doSplit = false;
    }

    if (!Array.isArray(options)) {
      options = [options];
    }

    this._currentInput.options(options.reduce(function(options, option) {
      var split = String(option).split(' ');

      if (doSplit && split.length === 2) {
        options.push(split[0], split[1]);
      } else {
        options.push(option);
      }

      return options;
    }, []));
    return this;
  };


  /**
   * Add custom output option(s)
   *
   * @example
   * command.outputOptions('option1');
   *
   * @example
   * command.outputOptions('option1', 'option2');
   *
   * @example
   * command.outputOptions(['option1', 'option2']);
   *
   * @method FfmpegCommand#outputOptions
   * @category Custom options
   * @aliases addOutputOption,addOutputOptions,addOption,addOptions,withOutputOption,withOutputOptions,withOption,withOptions,outputOption
   *
   * @param {...String} options option string(s) or string array
   * @return FfmpegCommand
   */
  proto.addOutputOption =
  proto.addOutputOptions =
  proto.addOption =
  proto.addOptions =
  proto.withOutputOption =
  proto.withOutputOptions =
  proto.withOption =
  proto.withOptions =
  proto.outputOption =
  proto.outputOptions = function(options) {
    var doSplit = true;

    if (arguments.length > 1) {
      options = [].slice.call(arguments);
      doSplit = false;
    }

    if (!Array.isArray(options)) {
      options = [options];
    }

    this._currentOutput.options(options.reduce(function(options, option) {
      var split = String(option).split(' ');

      if (doSplit && split.length === 2) {
        options.push(split[0], split[1]);
      } else {
        options.push(option);
      }

      return options;
    }, []));
    return this;
  };


  /**
   * Specify a complex filtergraph
   *
   * Calling this method will override any previously set filtergraph, but you can set
   * as many filters as needed in one call.
   *
   * @example <caption>Overlay an image over a video (using a filtergraph string)</caption>
   *   ffmpeg()
   *     .input('video.avi')
   *     .input('image.png')
   *     .complexFilter('[0:v][1:v]overlay[out]', ['out']);
   *
   * @example <caption>Overlay an image over a video (using a filter array)</caption>
   *   ffmpeg()
   *     .input('video.avi')
   *     .input('image.png')
   *     .complexFilter([{
   *       filter: 'overlay',
   *       inputs: ['0:v', '1:v'],
   *       outputs: ['out']
   *     }], ['out']);
   *
   * @example <caption>Split video into RGB channels and output a 3x1 video with channels side to side</caption>
   *  ffmpeg()
   *    .input('video.avi')
   *    .complexFilter([
   *      // Duplicate video stream 3 times into streams a, b, and c
   *      { filter: 'split', options: '3', outputs: ['a', 'b', 'c'] },
   *
   *      // Create stream 'red' by cancelling green and blue channels from stream 'a'
   *      { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' },
   *
   *      // Create stream 'green' by cancelling red and blue channels from stream 'b'
   *      { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' },
   *
   *      // Create stream 'blue' by cancelling red and green channels from stream 'c'
   *      { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' },
   *
   *      // Pad stream 'red' to 3x width, keeping the video on the left, and name output 'padded'
   *      { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' },
   *
   *      // Overlay 'green' onto 'padded', moving it to the center, and name output 'redgreen'
   *      { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen'},
   *
   *      // Overlay 'blue' onto 'redgreen', moving it to the right
   *      { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue']},
   *    ]);
   *
   * @method FfmpegCommand#complexFilter
   * @category Custom options
   * @aliases filterGraph
   *
   * @param {String|Array} spec filtergraph string or array of filter specification
   *   objects, each having the following properties:
   * @param {String} spec.filter filter name
   * @param {String|Array} [spec.inputs] (array of) input stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically choosing the first unused matching streams
   * @param {String|Array} [spec.outputs] (array of) output stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically assigning the output to the output file
   * @param {Object|String|Array} [spec.options] filter options, can be omitted to not set any options
   * @param {Array} [map] (array of) stream specifier(s) from the graph to include in
   *   ffmpeg output, defaults to ffmpeg automatically choosing the first matching streams.
   * @return FfmpegCommand
   */
  proto.filterGraph =
  proto.complexFilter = function(spec, map) {
    this._complexFilters.clear();

    if (!Array.isArray(spec)) {
      spec = [spec];
    }

    this._complexFilters('-filter_complex', utils.makeFilterStrings(spec).join(';'));

    if (Array.isArray(map)) {
      var self = this;
      map.forEach(function(streamSpec) {
        self._complexFilters('-map', streamSpec.replace(utils.streamRegexp, '[$1]'));
      });
    } else if (typeof map === 'string') {
      this._complexFilters('-map', map.replace(utils.streamRegexp, '[$1]'));
    }

    return this;
  };
};

================================================ FILE: doc/options_inputs.js.html ================================================ JSDoc: Source: options/inputs.js

Source: options/inputs.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');

/*
 *! Input-related methods
 */

module.exports = function(proto) {
  /**
   * Add an input to command
   *
   * Also switches "current input", that is the input that will be affected
   * by subsequent input-related methods.
   *
   * Note: only one stream input is supported for now.
   *
   * @method FfmpegCommand#input
   * @category Input
   * @aliases mergeAdd,addInput
   *
   * @param {String|Readable} source input file path or readable stream
   * @return FfmpegCommand
   */
  proto.mergeAdd =
  proto.addInput =
  proto.input = function(source) {
    var isFile = false;
    var isStream = false;

    if (typeof source !== 'string') {
      if (!('readable' in source) || !(source.readable)) {
        throw new Error('Invalid input');
      }

      var hasInputStream = this._inputs.some(function(input) {
        return input.isStream;
      });

      if (hasInputStream) {
        throw new Error('Only one input stream is supported');
      }

      isStream = true;
      source.pause();
    } else {
      var protocol = source.match(/^([a-z]{2,}):/i);
      isFile = !protocol || protocol[0] === 'file';
    }

    this._inputs.push(this._currentInput = {
      source: source,
      isFile: isFile,
      isStream: isStream,
      options: utils.args()
    });

    return this;
  };


  /**
   * Specify input format for the last specified input
   *
   * @method FfmpegCommand#inputFormat
   * @category Input
   * @aliases withInputFormat,fromFormat
   *
   * @param {String} format input format
   * @return FfmpegCommand
   */
  proto.withInputFormat =
  proto.inputFormat =
  proto.fromFormat = function(format) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-f', format);
    return this;
  };


  /**
   * Specify input FPS for the last specified input
   * (only valid for raw video formats)
   *
   * @method FfmpegCommand#inputFps
   * @category Input
   * @aliases withInputFps,withInputFPS,withFpsInput,withFPSInput,inputFPS,inputFps,fpsInput
   *
   * @param {Number} fps input FPS
   * @return FfmpegCommand
   */
  proto.withInputFps =
  proto.withInputFPS =
  proto.withFpsInput =
  proto.withFPSInput =
  proto.inputFPS =
  proto.inputFps =
  proto.fpsInput =
  proto.FPSInput = function(fps) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-r', fps);
    return this;
  };


  /**
   * Use native framerate for the last specified input
   *
   * @method FfmpegCommand#native
   * @category Input
   * @aliases nativeFramerate,withNativeFramerate
   *
   * @return FfmmegCommand
   */
  proto.nativeFramerate =
  proto.withNativeFramerate =
  proto.native = function() {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-re');
    return this;
  };


  /**
   * Specify input seek time for the last specified input
   *
   * @method FfmpegCommand#seekInput
   * @category Input
   * @aliases setStartTime,seekTo
   *
   * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.setStartTime =
  proto.seekInput = function(seek) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-ss', seek);

    return this;
  };


  /**
   * Loop over the last specified input
   *
   * @method FfmpegCommand#loop
   * @category Input
   *
   * @param {String|Number} [duration] loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.loop = function(duration) {
    if (!this._currentInput) {
      throw new Error('No input specified');
    }

    this._currentInput.options('-loop', '1');

    if (typeof duration !== 'undefined') {
      this.duration(duration);
    }

    return this;
  };
};

================================================ FILE: doc/options_misc.js.html ================================================ JSDoc: Source: options/misc.js

Source: options/misc.js

/*jshint node:true*/
'use strict';

var path = require('path');

/*
 *! Miscellaneous methods
 */

module.exports = function(proto) {
  /**
   * Use preset
   *
   * @method FfmpegCommand#preset
   * @category Miscellaneous
   * @aliases usingPreset
   *
   * @param {String|Function} preset preset name or preset function
   */
  proto.usingPreset =
  proto.preset = function(preset) {
    if (typeof preset === 'function') {
      preset(this);
    } else {
      try {
        var modulePath = path.join(this.options.presets, preset);
        var module = require(modulePath);

        if (typeof module.load === 'function') {
          module.load(this);
        } else {
          throw new Error('preset ' + modulePath + ' has no load() function');
        }
      } catch (err) {
        throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message);
      }
    }

    return this;
  };
};

================================================ FILE: doc/options_output.js.html ================================================ JSDoc: Source: options/output.js

Source: options/output.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Output-related methods
 */

module.exports = function(proto) {
  /**
   * Add output
   *
   * @method FfmpegCommand#output
   * @category Output
   * @aliases addOutput
   *
   * @param {String|Writable} target target file path or writable stream
   * @param {Object} [pipeopts={}] pipe options (only applies to streams)
   * @return FfmpegCommand
   */
  proto.addOutput =
  proto.output = function(target, pipeopts) {
    var isFile = false;

    if (!target && this._currentOutput) {
      // No target is only allowed when called from constructor
      throw new Error('Invalid output');
    }

    if (target && typeof target !== 'string') {
      if (!('writable' in target) || !(target.writable)) {
        throw new Error('Invalid output');
      }
    } else if (typeof target === 'string') {
      var protocol = target.match(/^([a-z]{2,}):/i);
      isFile = !protocol || protocol[0] === 'file';
    }

    if (target && !('target' in this._currentOutput)) {
      // For backwards compatibility, set target for first output
      this._currentOutput.target = target;
      this._currentOutput.isFile = isFile;
      this._currentOutput.pipeopts = pipeopts || {};
    } else {
      if (target && typeof target !== 'string') {
        var hasOutputStream = this._outputs.some(function(output) {
          return typeof output.target !== 'string';
        });

        if (hasOutputStream) {
          throw new Error('Only one output stream is supported');
        }
      }

      this._outputs.push(this._currentOutput = {
        target: target,
        isFile: isFile,
        flags: {},
        pipeopts: pipeopts || {}
      });

      var self = this;
      ['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) {
        self._currentOutput[key] = utils.args();
      });

      if (!target) {
        // Call from constructor: remove target key
        delete this._currentOutput.target;
      }
    }

    return this;
  };


  /**
   * Specify output seek time
   *
   * @method FfmpegCommand#seek
   * @category Input
   * @aliases seekOutput
   *
   * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.seekOutput =
  proto.seek = function(seek) {
    this._currentOutput.options('-ss', seek);
    return this;
  };


  /**
   * Set output duration
   *
   * @method FfmpegCommand#duration
   * @category Output
   * @aliases withDuration,setDuration
   *
   * @param {String|Number} duration duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.withDuration =
  proto.setDuration =
  proto.duration = function(duration) {
    this._currentOutput.options('-t', duration);
    return this;
  };


  /**
   * Set output format
   *
   * @method FfmpegCommand#format
   * @category Output
   * @aliases toFormat,withOutputFormat,outputFormat
   *
   * @param {String} format output format name
   * @return FfmpegCommand
   */
  proto.toFormat =
  proto.withOutputFormat =
  proto.outputFormat =
  proto.format = function(format) {
    this._currentOutput.options('-f', format);
    return this;
  };


  /**
   * Add stream mapping to output
   *
   * @method FfmpegCommand#map
   * @category Output
   *
   * @param {String} spec stream specification string, with optional square brackets
   * @return FfmpegCommand
   */
  proto.map = function(spec) {
    this._currentOutput.options('-map', spec.replace(utils.streamRegexp, '[$1]'));
    return this;
  };


  /**
   * Run flvtool2/flvmeta on output
   *
   * @method FfmpegCommand#flvmeta
   * @category Output
   * @aliases updateFlvMetadata
   *
   * @return FfmpegCommand
   */
  proto.updateFlvMetadata =
  proto.flvmeta = function() {
    this._currentOutput.flags.flvmeta = true;
    return this;
  };
};

================================================ FILE: doc/options_video.js.html ================================================ JSDoc: Source: options/video.js

Source: options/video.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Video-related methods
 */

module.exports = function(proto) {
  /**
   * Disable video in the output
   *
   * @method FfmpegCommand#noVideo
   * @category Video
   * @aliases withNoVideo
   *
   * @return FfmpegCommand
   */
  proto.withNoVideo =
  proto.noVideo = function() {
    this._currentOutput.video.clear();
    this._currentOutput.videoFilters.clear();
    this._currentOutput.video('-vn');

    return this;
  };


  /**
   * Specify video codec
   *
   * @method FfmpegCommand#videoCodec
   * @category Video
   * @aliases withVideoCodec
   *
   * @param {String} codec video codec name
   * @return FfmpegCommand
   */
  proto.withVideoCodec =
  proto.videoCodec = function(codec) {
    this._currentOutput.video('-vcodec', codec);
    return this;
  };


  /**
   * Specify video bitrate
   *
   * @method FfmpegCommand#videoBitrate
   * @category Video
   * @aliases withVideoBitrate
   *
   * @param {String|Number} bitrate video bitrate in kbps (with an optional 'k' suffix)
   * @param {Boolean} [constant=false] enforce constant bitrate
   * @return FfmpegCommand
   */
  proto.withVideoBitrate =
  proto.videoBitrate = function(bitrate, constant) {
    bitrate = ('' + bitrate).replace(/k?$/, 'k');

    this._currentOutput.video('-b:v', bitrate);
    if (constant) {
      this._currentOutput.video(
        '-maxrate', bitrate,
        '-minrate', bitrate,
        '-bufsize', '3M'
      );
    }

    return this;
  };


  /**
   * Specify custom video filter(s)
   *
   * Can be called both with one or many filters, or a filter array.
   *
   * @example
   * command.videoFilters('filter1');
   *
   * @example
   * command.videoFilters('filter1', 'filter2=param1=value1:param2=value2');
   *
   * @example
   * command.videoFilters(['filter1', 'filter2']);
   *
   * @example
   * command.videoFilters([
   *   {
   *     filter: 'filter1'
   *   },
   *   {
   *     filter: 'filter2',
   *     options: 'param=value:param=value'
   *   }
   * ]);
   *
   * @example
   * command.videoFilters(
   *   {
   *     filter: 'filter1',
   *     options: ['value1', 'value2']
   *   },
   *   {
   *     filter: 'filter2',
   *     options: { param1: 'value1', param2: 'value2' }
   *   }
   * );
   *
   * @method FfmpegCommand#videoFilters
   * @category Video
   * @aliases withVideoFilter,withVideoFilters,videoFilter
   *
   * @param {...String|String[]|Object[]} filters video filter strings, string array or
   *   filter specification array, each with the following properties:
   * @param {String} filters.filter filter name
   * @param {String|String[]|Object} [filters.options] filter option string, array, or object
   * @return FfmpegCommand
   */
  proto.withVideoFilter =
  proto.withVideoFilters =
  proto.videoFilter =
  proto.videoFilters = function(filters) {
    if (arguments.length > 1) {
      filters = [].slice.call(arguments);
    }

    if (!Array.isArray(filters)) {
      filters = [filters];
    }

    this._currentOutput.videoFilters(utils.makeFilterStrings(filters));

    return this;
  };


  /**
   * Specify output FPS
   *
   * @method FfmpegCommand#fps
   * @category Video
   * @aliases withOutputFps,withOutputFPS,withFpsOutput,withFPSOutput,withFps,withFPS,outputFPS,outputFps,fpsOutput,FPSOutput,FPS
   *
   * @param {Number} fps output FPS
   * @return FfmpegCommand
   */
  proto.withOutputFps =
  proto.withOutputFPS =
  proto.withFpsOutput =
  proto.withFPSOutput =
  proto.withFps =
  proto.withFPS =
  proto.outputFPS =
  proto.outputFps =
  proto.fpsOutput =
  proto.FPSOutput =
  proto.fps =
  proto.FPS = function(fps) {
    this._currentOutput.video('-r', fps);
    return this;
  };


  /**
   * Only transcode a certain number of frames
   *
   * @method FfmpegCommand#frames
   * @category Video
   * @aliases takeFrames,withFrames
   *
   * @param {Number} frames frame count
   * @return FfmpegCommand
   */
  proto.takeFrames =
  proto.withFrames =
  proto.frames = function(frames) {
    this._currentOutput.video('-vframes', frames);
    return this;
  };
};

================================================ FILE: doc/options_videosize.js.html ================================================ JSDoc: Source: options/videosize.js

Source: options/videosize.js

/*jshint node:true*/
'use strict';

/*
 *! Size helpers
 */


/**
 * Return filters to pad video to width*height,
 *
 * @param {Number} width output width
 * @param {Number} height output height
 * @param {Number} aspect video aspect ratio (without padding)
 * @param {Number} color padding color
 * @return scale/pad filters
 * @private
 */
function getScalePadFilters(width, height, aspect, color) {
  /*
    let a be the input aspect ratio, A be the requested aspect ratio

    if a > A, padding is done on top and bottom
    if a < A, padding is done on left and right
   */

  return [
    /*
      In both cases, we first have to scale the input to match the requested size.
      When using computed width/height, we truncate them to multiples of 2
     */
    {
      filter: 'scale',
      options: {
        w: 'if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2)',
        h: 'if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)'
      }
    },

    /*
      Then we pad the scaled input to match the target size
      (here iw and ih refer to the padding input, i.e the scaled output)
     */

    {
      filter: 'pad',
      options: {
        w: width,
        h: height,
        x: 'if(gt(a,' + aspect + '),0,(' + width + '-iw)/2)',
        y: 'if(lt(a,' + aspect + '),0,(' + height + '-ih)/2)',
        color: color
      }
    }
  ];
}


/**
 * Recompute size filters
 *
 * @param {Object} output
 * @param {String} key newly-added parameter name ('size', 'aspect' or 'pad')
 * @param {String} value newly-added parameter value
 * @return filter string array
 * @private
 */
function createSizeFilters(output, key, value) {
  // Store parameters
  var data = output.sizeData = output.sizeData || {};
  data[key] = value;

  if (!('size' in data)) {
    // No size requested, keep original size
    return [];
  }

  // Try to match the different size string formats
  var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/);
  var fixedWidth = data.size.match(/([0-9]+)x\?/);
  var fixedHeight = data.size.match(/\?x([0-9]+)/);
  var percentRatio = data.size.match(/\b([0-9]{1,3})%/);
  var width, height, aspect;

  if (percentRatio) {
    var ratio = Number(percentRatio[1]) / 100;
    return [{
      filter: 'scale',
      options: {
        w: 'trunc(iw*' + ratio + '/2)*2',
        h: 'trunc(ih*' + ratio + '/2)*2'
      }
    }];
  } else if (fixedSize) {
    // Round target size to multiples of 2
    width = Math.round(Number(fixedSize[1]) / 2) * 2;
    height = Math.round(Number(fixedSize[2]) / 2) * 2;

    aspect = width / height;

    if (data.pad) {
      return getScalePadFilters(width, height, aspect, data.pad);
    } else {
      // No autopad requested, rescale to target size
      return [{ filter: 'scale', options: { w: width, h: height }}];
    }
  } else if (fixedWidth || fixedHeight) {
    if ('aspect' in data) {
      // Specified aspect ratio
      width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect);
      height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect);

      // Round to multiples of 2
      width = Math.round(width / 2) * 2;
      height = Math.round(height / 2) * 2;

      if (data.pad) {
        return getScalePadFilters(width, height, data.aspect, data.pad);
      } else {
        // No autopad requested, rescale to target size
        return [{ filter: 'scale', options: { w: width, h: height }}];
      }
    } else {
      // Keep input aspect ratio

      if (fixedWidth) {
        return [{
          filter: 'scale',
          options: {
            w: Math.round(Number(fixedWidth[1]) / 2) * 2,
            h: 'trunc(ow/a/2)*2'
          }
        }];
      } else {
        return [{
          filter: 'scale',
          options: {
            w: 'trunc(oh*a/2)*2',
            h: Math.round(Number(fixedHeight[1]) / 2) * 2
          }
        }];
      }
    }
  } else {
    throw new Error('Invalid size specified: ' + data.size);
  }
}


/*
 *! Video size-related methods
 */

module.exports = function(proto) {
  /**
   * Keep display aspect ratio
   *
   * This method is useful when converting an input with non-square pixels to an output format
   * that does not support non-square pixels.  It rescales the input so that the display aspect
   * ratio is the same.
   *
   * @method FfmpegCommand#keepDAR
   * @category Video size
   * @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio
   *
   * @return FfmpegCommand
   */
  proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio
  proto.keepDisplayAspect =
  proto.keepDisplayAspectRatio =
  proto.keepDAR = function() {
    return this.videoFilters([
      {
        filter: 'scale',
        options: {
          w: 'if(gt(sar,1),iw*sar,iw)',
          h: 'if(lt(sar,1),ih/sar,ih)'
        }
      },
      {
        filter: 'setsar',
        options: '1'
      }
    ]);
  };


  /**
   * Set output size
   *
   * The 'size' parameter can have one of 4 forms:
   * - 'X%': rescale to xx % of the original size
   * - 'WxH': specify width and height
   * - 'Wx?': specify width and compute height from input aspect ratio
   * - '?xH': specify height and compute width from input aspect ratio
   *
   * Note: both dimensions will be truncated to multiples of 2.
   *
   * @method FfmpegCommand#size
   * @category Video size
   * @aliases withSize,setSize
   *
   * @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240'
   * @return FfmpegCommand
   */
  proto.withSize =
  proto.setSize =
  proto.size = function(size) {
    var filters = createSizeFilters(this._currentOutput, 'size', size);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };


  /**
   * Set output aspect ratio
   *
   * @method FfmpegCommand#aspect
   * @category Video size
   * @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio
   *
   * @param {String|Number} aspect aspect ratio (number or 'X:Y' string)
   * @return FfmpegCommand
   */
  proto.withAspect =
  proto.withAspectRatio =
  proto.setAspect =
  proto.setAspectRatio =
  proto.aspect =
  proto.aspectRatio = function(aspect) {
    var a = Number(aspect);
    if (isNaN(a)) {
      var match = aspect.match(/^(\d+):(\d+)$/);
      if (match) {
        a = Number(match[1]) / Number(match[2]);
      } else {
        throw new Error('Invalid aspect ratio: ' + aspect);
      }
    }

    var filters = createSizeFilters(this._currentOutput, 'aspect', a);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };


  /**
   * Enable auto-padding the output
   *
   * @method FfmpegCommand#autopad
   * @category Video size
   * @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad
   *
   * @param {Boolean} [pad=true] enable/disable auto-padding
   * @param {String} [color='black'] pad color
   */
  proto.applyAutopadding =
  proto.applyAutoPadding =
  proto.applyAutopad =
  proto.applyAutoPad =
  proto.withAutopadding =
  proto.withAutoPadding =
  proto.withAutopad =
  proto.withAutoPad =
  proto.autoPad =
  proto.autopad = function(pad, color) {
    // Allow autopad(color)
    if (typeof pad === 'string') {
      color = pad;
      pad = true;
    }

    // Allow autopad() and autopad(undefined, color)
    if (typeof pad === 'undefined') {
      pad = true;
    }

    var filters = createSizeFilters(this._currentOutput, 'pad', pad ? color || 'black' : false);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };
};

================================================ FILE: doc/output.js.html ================================================ JSDoc: Source: options/output.js

Source: options/output.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Output-related methods
 */

module.exports = function(proto) {
  /**
   * Add output
   *
   * @method FfmpegCommand#output
   * @category Output
   * @aliases addOutput
   *
   * @param {String|Writable} target target file path or writable stream
   * @param {Object} [pipeopts={}] pipe options (only applies to streams)
   * @return FfmpegCommand
   */
  proto.addOutput =
  proto.output = function(target, pipeopts) {
    var isFile = false;

    if (!target && this._currentOutput) {
      // No target is only allowed when called from constructor
      throw new Error('Invalid output');
    }

    if (target && typeof target !== 'string') {
      if (!('writable' in target) || !(target.writable)) {
        throw new Error('Invalid output');
      }
    } else if (typeof target === 'string') {
      var protocol = target.match(/^([a-z]{2,}):/i);
      isFile = !protocol || protocol[0] === 'file';
    }

    if (target && !('target' in this._currentOutput)) {
      // For backwards compatibility, set target for first output
      this._currentOutput.target = target;
      this._currentOutput.isFile = isFile;
      this._currentOutput.pipeopts = pipeopts || {};
    } else {
      if (target && typeof target !== 'string') {
        var hasOutputStream = this._outputs.some(function(output) {
          return typeof output.target !== 'string';
        });

        if (hasOutputStream) {
          throw new Error('Only one output stream is supported');
        }
      }

      this._outputs.push(this._currentOutput = {
        target: target,
        isFile: isFile,
        flags: {},
        pipeopts: pipeopts || {}
      });

      var self = this;
      ['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) {
        self._currentOutput[key] = utils.args();
      });

      if (!target) {
        // Call from constructor: remove target key
        delete this._currentOutput.target;
      }
    }

    return this;
  };


  /**
   * Specify output seek time
   *
   * @method FfmpegCommand#seek
   * @category Input
   * @aliases seekOutput
   *
   * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.seekOutput =
  proto.seek = function(seek) {
    this._currentOutput.options('-ss', seek);
    return this;
  };


  /**
   * Set output duration
   *
   * @method FfmpegCommand#duration
   * @category Output
   * @aliases withDuration,setDuration
   *
   * @param {String|Number} duration duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
   * @return FfmpegCommand
   */
  proto.withDuration =
  proto.setDuration =
  proto.duration = function(duration) {
    this._currentOutput.options('-t', duration);
    return this;
  };


  /**
   * Set output format
   *
   * @method FfmpegCommand#format
   * @category Output
   * @aliases toFormat,withOutputFormat,outputFormat
   *
   * @param {String} format output format name
   * @return FfmpegCommand
   */
  proto.toFormat =
  proto.withOutputFormat =
  proto.outputFormat =
  proto.format = function(format) {
    this._currentOutput.options('-f', format);
    return this;
  };


  /**
   * Add stream mapping to output
   *
   * @method FfmpegCommand#map
   * @category Output
   *
   * @param {String} spec stream specification string, with optional square brackets
   * @return FfmpegCommand
   */
  proto.map = function(spec) {
    this._currentOutput.options('-map', spec.replace(utils.streamRegexp, '[$1]'));
    return this;
  };


  /**
   * Run flvtool2/flvmeta on output
   *
   * @method FfmpegCommand#flvmeta
   * @category Output
   * @aliases updateFlvMetadata
   *
   * @return FfmpegCommand
   */
  proto.updateFlvMetadata =
  proto.flvmeta = function() {
    this._currentOutput.flags.flvmeta = true;
    return this;
  };
};

================================================ FILE: doc/processor.js.html ================================================ JSDoc: Source: processor.js

Source: processor.js

/*jshint node:true*/
'use strict';

var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
var async = require('async');
var utils = require('./utils');

var nlRegexp = /\r\n|\r|\n/g;

/*
 *! Processor methods
 */


/**
 * Run ffprobe asynchronously and store data in command
 *
 * @param {FfmpegCommand} command
 * @private
 */
function runFfprobe(command) {
  command.ffprobe(0, function(err, data) {
    command._ffprobeData = data;
  });
}


module.exports = function(proto) {
  /**
   * Emitted just after ffmpeg has been spawned.
   *
   * @event FfmpegCommand#start
   * @param {String} command ffmpeg command line
   */

  /**
   * Emitted when ffmpeg reports progress information
   *
   * @event FfmpegCommand#progress
   * @param {Object} progress progress object
   * @param {Number} progress.frames number of frames transcoded
   * @param {Number} progress.currentFps current processing speed in frames per second
   * @param {Number} progress.currentKbps current output generation speed in kilobytes per second
   * @param {Number} progress.targetSize current output file size
   * @param {String} progress.timemark current video timemark
   * @param {Number} [progress.percent] processing progress (may not be available depending on input)
   */

  /**
   * Emitted when ffmpeg outputs to stderr
   *
   * @event FfmpegCommand#stderr
   * @param {String} line stderr output line
   */

  /**
   * Emitted when ffmpeg reports input codec data
   *
   * @event FfmpegCommand#codecData
   * @param {Object} codecData codec data object
   * @param {String} codecData.format input format name
   * @param {String} codecData.audio input audio codec name
   * @param {String} codecData.audio_details input audio codec parameters
   * @param {String} codecData.video input video codec name
   * @param {String} codecData.video_details input video codec parameters
   */

  /**
   * Emitted when an error happens when preparing or running a command
   *
   * @event FfmpegCommand#error
   * @param {Error} error error object
   * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
   * @param {String|null} stderr ffmpeg stderr
   */

  /**
   * Emitted when a command finishes processing
   *
   * @event FfmpegCommand#end
   * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
   * @param {String|null} stderr ffmpeg stderr
   */


  /**
   * Spawn an ffmpeg process
   *
   * The 'options' argument may contain the following keys:
   * - 'niceness': specify process niceness, ignored on Windows (default: 0)
   * - `cwd`: change working directory
   * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
   * - 'stdoutLines': override command limit (default: use command limit)
   *
   * The 'processCB' callback, if present, is called as soon as the process is created and
   * receives a nodejs ChildProcess object.  It may not be called at all if an error happens
   * before spawning the process.
   *
   * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
   *
   * @method FfmpegCommand#_spawnFfmpeg
   * @param {Array} args ffmpeg command line argument list
   * @param {Object} [options] spawn options (see above)
   * @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created
   * @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished
   * @private
   */
  proto._spawnFfmpeg = function(args, options, processCB, endCB) {
    // Enable omitting options
    if (typeof options === 'function') {
      endCB = processCB;
      processCB = options;
      options = {};
    }

    // Enable omitting processCB
    if (typeof endCB === 'undefined') {
      endCB = processCB;
      processCB = function() {};
    }

    var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;

    // Find ffmpeg
    this._getFfmpegPath(function(err, command) {
      if (err) {
        return endCB(err);
      } else if (!command || command.length === 0) {
        return endCB(new Error('Cannot find ffmpeg'));
      }

      // Apply niceness
      if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
        args.unshift('-n', options.niceness, command);
        command = 'nice';
      }

      var stdoutRing = utils.linesRing(maxLines);
      var stdoutClosed = false;

      var stderrRing = utils.linesRing(maxLines);
      var stderrClosed = false;

      // Spawn process
      var ffmpegProc = spawn(command, args, options);

      if (ffmpegProc.stderr) {
        ffmpegProc.stderr.setEncoding('utf8');
      }

      ffmpegProc.on('error', function(err) {
        endCB(err);
      });

      // Ensure we wait for captured streams to end before calling endCB
      var exitError = null;
      function handleExit(err) {
        if (err) {
          exitError = err;
        }

        if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
          endCB(exitError, stdoutRing, stderrRing);
        }
      }

      // Handle process exit
      var processExited = false;
      ffmpegProc.on('exit', function(code, signal) {
        processExited = true;

        if (signal) {
          handleExit(new Error('ffmpeg was killed with signal ' + signal));
        } else if (code) {
          handleExit(new Error('ffmpeg exited with code ' + code));
        } else {
          handleExit();
        }
      });

      // Capture stdout if specified
      if (options.captureStdout) {
        ffmpegProc.stdout.on('data', function(data) {
          stdoutRing.append(data);
        });

        ffmpegProc.stdout.on('close', function() {
          stdoutRing.close();
          stdoutClosed = true;
          handleExit();
        });
      }

      // Capture stderr if specified
      ffmpegProc.stderr.on('data', function(data) {
        stderrRing.append(data);
      });

      ffmpegProc.stderr.on('close', function() {
        stderrRing.close();
        stderrClosed = true;
        handleExit();
      });

      // Call process callback
      processCB(ffmpegProc, stdoutRing, stderrRing);
    });
  };


  /**
   * Build the argument list for an ffmpeg command
   *
   * @method FfmpegCommand#_getArguments
   * @return argument list
   * @private
   */
  proto._getArguments = function() {
    var complexFilters = this._complexFilters.get();

    var fileOutput = this._outputs.some(function(output) {
      return output.isFile;
    });

    return [].concat(
        // Inputs and input options
        this._inputs.reduce(function(args, input) {
          var source = (typeof input.source === 'string') ? input.source : 'pipe:0';

          // For each input, add input options, then '-i <source>'
          return args.concat(
            input.options.get(),
            ['-i', source]
          );
        }, []),

        // Global options
        this._global.get(),

        // Overwrite if we have file outputs
        fileOutput ? ['-y'] : [],

        // Complex filters
        complexFilters,

        // Outputs, filters and output options
        this._outputs.reduce(function(args, output) {
          var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get());
          var audioFilters = output.audioFilters.get();
          var videoFilters = output.videoFilters.get().concat(sizeFilters);
          var outputArg;

          if (!output.target) {
            outputArg = [];
          } else if (typeof output.target === 'string') {
            outputArg = [output.target];
          } else {
            outputArg = ['pipe:1'];
          }

          return args.concat(
            output.audio.get(),
            audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
            output.video.get(),
            videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
            output.options.get(),
            outputArg
          );
        }, [])
      );
  };


  /**
   * Prepare execution of an ffmpeg command
   *
   * Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
   * then builds the argument list for ffmpeg and pass them to 'callback'.
   *
   * @method FfmpegCommand#_prepare
   * @param {Function} callback callback with signature (err, args)
   * @param {Boolean} [readMetadata=false] read metadata before processing
   * @private
   */
  proto._prepare = function(callback, readMetadata) {
    var self = this;

    async.waterfall([
      // Check codecs and formats
      function(cb) {
        self._checkCapabilities(cb);
      },

      // Read metadata if required
      function(cb) {
        if (!readMetadata) {
          return cb();
        }

        self.ffprobe(0, function(err, data) {
          if (!err) {
            self._ffprobeData = data;
          }

          cb();
        });
      },

      // Check for flvtool2/flvmeta if necessary
      function(cb) {
        var flvmeta = self._outputs.some(function(output) {
          // Remove flvmeta flag on non-file output
          if (output.flags.flvmeta && !output.isFile) {
            self.logger.warn('Updating flv metadata is only supported for files');
            output.flags.flvmeta = false;
          }

          return output.flags.flvmeta;
        });

        if (flvmeta) {
          self._getFlvtoolPath(function(err) {
            cb(err);
          });
        } else {
          cb();
        }
      },

      // Build argument list
      function(cb) {
        var args;
        try {
          args = self._getArguments();
        } catch(e) {
          return cb(e);
        }

        cb(null, args);
      },

      // Add "-strict experimental" option where needed
      function(args, cb) {
        self.availableEncoders(function(err, encoders) {
          for (var i = 0; i < args.length; i++) {
            if (args[i] === '-acodec' || args[i] === '-vcodec') {
              i++;

              if ((args[i] in encoders) && encoders[args[i]].experimental) {
                args.splice(i + 1, 0, '-strict', 'experimental');
                i += 2;
              }
            }
          }

          cb(null, args);
        });
      }
    ], callback);

    if (!readMetadata) {
      // Read metadata as soon as 'progress' listeners are added

      if (this.listeners('progress').length > 0) {
        // Read metadata in parallel
        runFfprobe(this);
      } else {
        // Read metadata as soon as the first 'progress' listener is added
        this.once('newListener', function(event) {
          if (event === 'progress') {
            runFfprobe(this);
          }
        });
      }
    }
  };


  /**
   * Run ffmpeg command
   *
   * @method FfmpegCommand#run
   * @category Processing
   * @aliases exec,execute
   */
  proto.exec =
  proto.execute =
  proto.run = function() {
    var self = this;

    // Check if at least one output is present
    var outputPresent = this._outputs.some(function(output) {
      return 'target' in output;
    });

    if (!outputPresent) {
      throw new Error('No output specified');
    }

    // Get output stream if any
    var outputStream = this._outputs.filter(function(output) {
      return typeof output.target !== 'string';
    })[0];

    // Get input stream if any
    var inputStream = this._inputs.filter(function(input) {
      return typeof input.source !== 'string';
    })[0];

    // Ensure we send 'end' or 'error' only once
    var ended = false;
    function emitEnd(err, stdout, stderr) {
      if (!ended) {
        ended = true;

        if (err) {
          self.emit('error', err, stdout, stderr);
        } else {
          self.emit('end', stdout, stderr);
        }
      }
    }

    self._prepare(function(err, args) {
      if (err) {
        return emitEnd(err);
      }

      // Run ffmpeg
      self._spawnFfmpeg(
        args,
        {
          captureStdout: !outputStream,
          niceness: self.options.niceness,
          cwd: self.options.cwd
        },

        function processCB(ffmpegProc, stdoutRing, stderrRing) {
          self.ffmpegProc = ffmpegProc;
          self.emit('start', 'ffmpeg ' + args.join(' '));

          // Pipe input stream if any
          if (inputStream) {
            inputStream.source.on('error', function(err) {
              emitEnd(new Error('Input stream error: ' + err.message));
              ffmpegProc.kill();
            });

            inputStream.source.resume();
            inputStream.source.pipe(ffmpegProc.stdin);

            // Set stdin error handler on ffmpeg (prevents nodejs catching the error, but
            // ffmpeg will fail anyway, so no need to actually handle anything)
            ffmpegProc.stdin.on('error', function() {});
          }

          // Setup timeout if requested
          var processTimer;
          if (self.options.timeout) {
            processTimer = setTimeout(function() {
              var msg = 'process ran into a timeout (' + self.options.timeout + 's)';

              emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get());
              ffmpegProc.kill();
            }, self.options.timeout * 1000);
          }


          if (outputStream) {
            // Pipe ffmpeg stdout to output stream
            ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);

            // Handle output stream events
            outputStream.target.on('close', function() {
              self.logger.debug('Output stream closed, scheduling kill for ffmpgeg process');

              // Don't kill process yet, to give a chance to ffmpeg to
              // terminate successfully first  This is necessary because
              // under load, the process 'exit' event sometimes happens
              // after the output stream 'close' event.
              setTimeout(function() {
                emitEnd(new Error('Output stream closed'));
                ffmpegProc.kill();
              }, 20);
            });

            outputStream.target.on('error', function(err) {
              self.logger.debug('Output stream error, killing ffmpgeg process');
              emitEnd(new Error('Output stream error: ' + err.message), stdoutRing.get(), stderrRing.get());
              ffmpegProc.kill();
            });
          }

          // Setup stderr handling
          if (stderrRing) {

            // 'stderr' event
            if (self.listeners('stderr').length) {
              stderrRing.callback(function(line) {
                self.emit('stderr', line);
              });
            }

            // 'codecData' event
            if (self.listeners('codecData').length) {
              var codecDataSent = false;
              var codecObject = {};

              stderrRing.callback(function(line) {
                if (!codecDataSent)
                  codecDataSent = utils.extractCodecData(self, line, codecObject);
              });
            }

            // 'progress' event
            if (self.listeners('progress').length) {
              var duration = 0;

              if (self._ffprobeData && self._ffprobeData.format && self._ffprobeData.format.duration) {
                duration = Number(self._ffprobeData.format.duration);
              }

              stderrRing.callback(function(line) {
                utils.extractProgress(self, line, duration);
              });
            }
          }
        },

        function endCB(err, stdoutRing, stderrRing) {
          delete self.ffmpegProc;

          if (err) {
            if (err.message.match(/ffmpeg exited with code/)) {
              // Add ffmpeg error message
              err.message += ': ' + utils.extractError(stderrRing.get());
            }

            emitEnd(err, stdoutRing.get(), stderrRing.get());
          } else {
            // Find out which outputs need flv metadata
            var flvmeta = self._outputs.filter(function(output) {
              return output.flags.flvmeta;
            });

            if (flvmeta.length) {
              self._getFlvtoolPath(function(err, flvtool) {
                if (err) {
                  return emitEnd(err);
                }

                async.each(
                  flvmeta,
                  function(output, cb) {
                    spawn(flvtool, ['-U', output.target])
                      .on('error', function(err) {
                        cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message));
                      })
                      .on('exit', function(code, signal) {
                        if (code !== 0 || signal) {
                          cb(
                            new Error(flvtool + ' ' +
                              (signal ? 'received signal ' + signal
                                      : 'exited with code ' + code)) +
                              ' when running on ' + output.target
                          );
                        } else {
                          cb();
                        }
                      });
                  },
                  function(err) {
                    if (err) {
                      emitEnd(err);
                    } else {
                      emitEnd(null, stdoutRing.get(), stderrRing.get());
                    }
                  }
                );
              });
            } else {
              emitEnd(null, stdoutRing.get(), stderrRing.get());
            }
          }
        }
      );
    });
  };


  /**
   * Renice current and/or future ffmpeg processes
   *
   * Ignored on Windows platforms.
   *
   * @method FfmpegCommand#renice
   * @category Processing
   *
   * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
   * @return FfmpegCommand
   */
  proto.renice = function(niceness) {
    if (!utils.isWindows) {
      niceness = niceness || 0;

      if (niceness < -20 || niceness > 20) {
        this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
      }

      niceness = Math.min(20, Math.max(-20, niceness));
      this.options.niceness = niceness;

      if (this.ffmpegProc) {
        var logger = this.logger;
        var pid = this.ffmpegProc.pid;
        var renice = spawn('renice', [niceness, '-p', pid]);

        renice.on('error', function(err) {
          logger.warn('could not renice process ' + pid + ': ' + err.message);
        });

        renice.on('exit', function(code, signal) {
          if (signal) {
            logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
          } else if (code) {
            logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
          } else {
            logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
          }
        });
      }
    }

    return this;
  };


  /**
   * Kill current ffmpeg process, if any
   *
   * @method FfmpegCommand#kill
   * @category Processing
   *
   * @param {String} [signal=SIGKILL] signal name
   * @return FfmpegCommand
   */
  proto.kill = function(signal) {
    if (!this.ffmpegProc) {
      this.logger.warn('No running ffmpeg process, cannot send signal');
    } else {
      this.ffmpegProc.kill(signal || 'SIGKILL');
    }

    return this;
  };
};

================================================ FILE: doc/recipes.js.html ================================================ JSDoc: Source: recipes.js

Source: recipes.js

/*jshint node:true*/
'use strict';

var fs = require('fs');
var path = require('path');
var PassThrough = require('stream').PassThrough;
var async = require('async');
var utils = require('./utils');


/*
 * Useful recipes for commands
 */

module.exports = function recipes(proto) {
  /**
   * Execute ffmpeg command and save output to a file
   *
   * @method FfmpegCommand#save
   * @category Processing
   * @aliases saveToFile
   *
   * @param {String} output file path
   * @return FfmpegCommand
   */
  proto.saveToFile =
  proto.save = function(output) {
    this.output(output).run();
    return this;
  };


  /**
   * Execute ffmpeg command and save output to a stream
   *
   * If 'stream' is not specified, a PassThrough stream is created and returned.
   * 'options' will be used when piping ffmpeg output to the output stream
   * (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)
   *
   * @method FfmpegCommand#pipe
   * @category Processing
   * @aliases stream,writeToStream
   *
   * @param {stream.Writable} [stream] output stream
   * @param {Object} [options={}] pipe options
   * @return Output stream
   */
  proto.writeToStream =
  proto.pipe =
  proto.stream = function(stream, options) {
    if (stream && !('writable' in stream)) {
      options = stream;
      stream = undefined;
    }

    if (!stream) {
      if (process.version.match(/v0\.8\./)) {
        throw new Error('PassThrough stream is not supported on node v0.8');
      }

      stream = new PassThrough();
    }

    this.output(stream, options).run();
    return stream;
  };


  /**
   * Generate images from a video
   *
   * Note: this method makes the command emit a 'filenames' event with an array of
   * the generated image filenames.
   *
   * @method FfmpegCommand#screenshots
   * @category Processing
   * @aliases takeScreenshots,thumbnail,thumbnails,screenshot
   *
   * @param {Number|Object} [config=1] screenshot count or configuration object with
   *   the following keys:
   * @param {Number} [config.count] number of screenshots to take; using this option
   *   takes screenshots at regular intervals (eg. count=4 would take screens at 20%, 40%,
   *   60% and 80% of the video length).
   * @param {String} [config.folder='.'] output folder
   * @param {String} [config.filename='tn.png'] output filename pattern, may contain the following
   *   tokens:
   *   - '%s': offset in seconds
   *   - '%w': screenshot width
   *   - '%h': screenshot height
   *   - '%r': screenshot resolution (same as '%wx%h')
   *   - '%f': input filename
   *   - '%b': input basename (filename w/o extension)
   *   - '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`)
   * @param {Number[]|String[]} [config.timemarks] array of timemarks to take screenshots
   *   at; each timemark may be a number of seconds, a '[[hh:]mm:]ss[.xxx]' string or a
   *   'XX%' string.  Overrides 'count' if present.
   * @param {Number[]|String[]} [config.timestamps] alias for 'timemarks'
   * @param {Boolean} [config.fastSeek] use fast seek (less accurate)
   * @param {String} [config.size] screenshot size, with the same syntax as {@link FfmpegCommand#size}
   * @param {String} [folder] output folder (legacy alias for 'config.folder')
   * @return FfmpegCommand
   */
  proto.takeScreenshots =
  proto.thumbnail =
  proto.thumbnails =
  proto.screenshot =
  proto.screenshots = function(config, folder) {
    var self = this;
    var source = this._currentInput.source;
    config = config || { count: 1 };

    // Accept a number of screenshots instead of a config object
    if (typeof config === 'number') {
      config = {
        count: config
      };
    }

    // Accept a second 'folder' parameter instead of config.folder
    if (!('folder' in config)) {
      config.folder = folder || '.';
    }

    // Accept 'timestamps' instead of 'timemarks'
    if ('timestamps' in config) {
      config.timemarks = config.timestamps;
    }

    // Compute timemarks from count if not present
    if (!('timemarks' in config)) {
      if (!config.count) {
        throw new Error('Cannot take screenshots: neither a count nor a timemark list are specified');
      }

      var interval = 100 / (1 + config.count);
      config.timemarks = [];
      for (var i = 0; i < config.count; i++) {
        config.timemarks.push((interval * (i + 1)) + '%');
      }
    }

    // Parse size option
    if ('size' in config) {
      var fixedSize = config.size.match(/^(\d+)x(\d+)$/);
      var fixedWidth = config.size.match(/^(\d+)x\?$/);
      var fixedHeight = config.size.match(/^\?x(\d+)$/);
      var percentSize = config.size.match(/^(\d+)%$/);

      if (!fixedSize && !fixedWidth && !fixedHeight && !percentSize) {
        throw new Error('Invalid size parameter: ' + config.size);
      }
    }

    // Metadata helper
    var metadata;
    function getMetadata(cb) {
      if (metadata) {
        cb(null, metadata);
      } else {
        self.ffprobe(function(err, meta) {
          metadata = meta;
          cb(err, meta);
        });
      }
    }

    async.waterfall([
      // Compute percent timemarks if any
      function computeTimemarks(next) {
        if (config.timemarks.some(function(t) { return ('' + t).match(/^[\d.]+%$/); })) {
          if (typeof source !== 'string') {
            return next(new Error('Cannot compute screenshot timemarks with an input stream, please specify fixed timemarks'));
          }

          getMetadata(function(err, meta) {
            if (err) {
              next(err);
            } else {
              // Select video stream with the highest resolution
              var vstream = meta.streams.reduce(function(biggest, stream) {
                if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) {
                  return stream;
                } else {
                  return biggest;
                }
              }, { width: 0, height: 0 });

              if (vstream.width === 0) {
                return next(new Error('No video stream in input, cannot take screenshots'));
              }

              var duration = Number(vstream.duration);
              if (isNaN(duration)) {
                duration = Number(meta.format.duration);
              }

              if (isNaN(duration)) {
                return next(new Error('Could not get input duration, please specify fixed timemarks'));
              }

              config.timemarks = config.timemarks.map(function(mark) {
                if (('' + mark).match(/^([\d.]+)%$/)) {
                  return duration * parseFloat(mark) / 100;
                } else {
                  return mark;
                }
              });

              next();
            }
          });
        } else {
          next();
        }
      },

      // Turn all timemarks into numbers and sort them
      function normalizeTimemarks(next) {
        config.timemarks = config.timemarks.map(function(mark) {
          return utils.timemarkToSeconds(mark);
        }).sort(function(a, b) { return a - b; });

        next();
      },

      // Add '_%i' to pattern when requesting multiple screenshots and no variable token is present
      function fixPattern(next) {
        var pattern = config.filename || 'tn.png';

        if (pattern.indexOf('.') === -1) {
          pattern += '.png';
        }

        if (config.timemarks.length > 1 && !pattern.match(/%(s|0*i)/)) {
          var ext = path.extname(pattern);
          pattern = path.join(path.dirname(pattern), path.basename(pattern, ext) + '_%i' + ext);
        }

        next(null, pattern);
      },

      // Replace filename tokens (%f, %b) in pattern
      function replaceFilenameTokens(pattern, next) {
        if (pattern.match(/%[bf]/)) {
          if (typeof source !== 'string') {
            return next(new Error('Cannot replace %f or %b when using an input stream'));
          }

          pattern = pattern
            .replace(/%f/g, path.basename(source))
            .replace(/%b/g, path.basename(source, path.extname(source)));
        }

        next(null, pattern);
      },

      // Compute size if needed
      function getSize(pattern, next) {
        if (pattern.match(/%[whr]/)) {
          if (fixedSize) {
            return next(null, pattern, fixedSize[1], fixedSize[2]);
          }

          getMetadata(function(err, meta) {
            if (err) {
              return next(new Error('Could not determine video resolution to replace %w, %h or %r'));
            }

            var vstream = meta.streams.reduce(function(biggest, stream) {
              if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) {
                return stream;
              } else {
                return biggest;
              }
            }, { width: 0, height: 0 });

            if (vstream.width === 0) {
              return next(new Error('No video stream in input, cannot replace %w, %h or %r'));
            }

            var width = vstream.width;
            var height = vstream.height;

            if (fixedWidth) {
              height = height * Number(fixedWidth[1]) / width;
              width = Number(fixedWidth[1]);
            } else if (fixedHeight) {
              width = width * Number(fixedHeight[1]) / height;
              height = Number(fixedHeight[1]);
            } else if (percentSize) {
              width = width * Number(percentSize[1]) / 100;
              height = height * Number(percentSize[1]) / 100;
            }

            next(null, pattern, Math.round(width / 2) * 2, Math.round(height / 2) * 2);
          });
        } else {
          next(null, pattern, -1, -1);
        }
      },

      // Replace size tokens (%w, %h, %r) in pattern
      function replaceSizeTokens(pattern, width, height, next) {
        pattern = pattern
          .replace(/%r/g, '%wx%h')
          .replace(/%w/g, width)
          .replace(/%h/g, height);

        next(null, pattern);
      },

      // Replace variable tokens in pattern (%s, %i) and generate filename list
      function replaceVariableTokens(pattern, next) {
        var filenames = config.timemarks.map(function(t, i) {
          return pattern
            .replace(/%s/g, utils.timemarkToSeconds(t))
            .replace(/%(0*)i/g, function(match, padding) {
              var idx = '' + (i + 1);
              return padding.substr(0, Math.max(0, padding.length + 1 - idx.length)) + idx;
            });
        });

        self.emit('filenames', filenames);
        next(null, filenames);
      },

      // Create output directory
      function createDirectory(filenames, next) {
        fs.exists(config.folder, function(exists) {
          if (!exists) {
            fs.mkdir(config.folder, function(err) {
              if (err) {
                next(err);
              } else {
                next(null, filenames);
              }
            });
          } else {
            next(null, filenames);
          }
        });
      }
    ], function runCommand(err, filenames) {
      if (err) {
        return self.emit('error', err);
      }

      var count = config.timemarks.length;
      var split;
      var filters = [split = {
        filter: 'split',
        options: count,
        outputs: []
      }];

      if ('size' in config) {
        // Set size to generate size filters
        self.size(config.size);

        // Get size filters and chain them with 'sizeN' stream names
        var sizeFilters =  self._currentOutput.sizeFilters.get().map(function(f, i) {
          if (i > 0) {
            f.inputs = 'size' + (i - 1);
          }

          f.outputs = 'size' + i;

          return f;
        });

        // Input last size filter output into split filter
        split.inputs = 'size' + (sizeFilters.length - 1);

        // Add size filters in front of split filter
        filters = sizeFilters.concat(filters);

        // Remove size filters
        self._currentOutput.sizeFilters.clear();
      }

      var first = 0;
      for (var i = 0; i < count; i++) {
        var stream = 'screen' + i;
        split.outputs.push(stream);

        if (i === 0) {
          first = config.timemarks[i];
          self.seekInput(first);
        }

        self.output(path.join(config.folder, filenames[i]))
          .frames(1)
          .map(stream);

        if (i > 0) {
          self.seek(config.timemarks[i] - first);
        }
      }

      self.complexFilter(filters);
      self.run();
    });

    return this;
  };


  /**
   * Merge (concatenate) inputs to a single file
   *
   * @method FfmpegCommand#concat
   * @category Processing
   * @aliases concatenate,mergeToFile
   *
   * @param {String|Writable} target output file or writable stream
   * @param {Object} [options] pipe options (only used when outputting to a writable stream)
   * @return FfmpegCommand
   */
  proto.mergeToFile =
  proto.concatenate =
  proto.concat = function(target, options) {
    // Find out which streams are present in the first non-stream input
    var fileInput = this._inputs.filter(function(input) {
      return !input.isStream;
    })[0];

    var self = this;
    this.ffprobe(this._inputs.indexOf(fileInput), function(err, data) {
      if (err) {
        return self.emit('error', err);
      }

      var hasAudioStreams = data.streams.some(function(stream) {
        return stream.codec_type === 'audio';
      });

      var hasVideoStreams = data.streams.some(function(stream) {
        return stream.codec_type === 'video';
      });

      // Setup concat filter and start processing
      self.output(target, options)
        .complexFilter({
          filter: 'concat',
          options: {
            n: self._inputs.length,
            v: hasVideoStreams ? 1 : 0,
            a: hasAudioStreams ? 1 : 0
          }
        })
        .run();
    });

    return this;
  };
};

================================================ FILE: doc/scripts/linenumber.js ================================================ /*global document */ (function() { var source = document.getElementsByClassName('prettyprint source linenums'); var i = 0; var lineNumber = 0; var lineId; var lines; var totalLines; var anchorHash; if (source && source[0]) { anchorHash = document.location.hash.substring(1); lines = source[0].getElementsByTagName('li'); totalLines = lines.length; for (; i < totalLines; i++) { lineNumber++; lineId = 'line' + lineNumber; lines[i].id = lineId; if (lineId === anchorHash) { lines[i].className += ' selected'; } } } })(); ================================================ FILE: doc/scripts/prettify/Apache-License-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: doc/scripts/prettify/lang-css.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); ================================================ FILE: doc/scripts/prettify/prettify.js ================================================ var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p h2 { margin-top: 6px; } h3 { font-size: 150%; font-weight: bold; letter-spacing: -0.01em; margin-top: 16px; margin: 50px 0 3px 0; } h4 { font-size: 130%; font-weight: bold; letter-spacing: -0.01em; margin-top: 16px; margin: 18px 0 3px 0; color: #526492; } h5, .container-overview .subsection-title { font-size: 120%; font-weight: bold; letter-spacing: -0.01em; margin: 8px 0 3px -16px; } h6 { font-size: 100%; letter-spacing: -0.01em; margin: 6px 0 3px 0; font-style: italic; } article > dl, article > pre { margin-left: 2em; } .ancestors { color: #999; } .ancestors a { color: #999 !important; text-decoration: none; } .important { font-weight: bold; color: #950B02; } .yes-def { text-indent: -1000px; } .type-signature { color: #aaa; } .name, .signature { font-family: Consolas, "Lucida Console", Monaco, monospace; } .details { margin-top: 14px; border-left: 2px solid #DDD; } .details dt { width:100px; float:left; padding-left: 10px; padding-top: 6px; } .details dd { margin-left: 50px; } .details ul { margin: 0; } .details ul { list-style-type: none; } .details li { margin-left: 30px; padding-top: 6px; } .details pre.prettyprint { margin: 0 } .details .object-value { padding-top: 0; } .description { margin-bottom: 1em; margin-left: -16px; margin-top: 1em; } .code-caption { font-style: italic; font-family: Palatino, 'Palatino Linotype', serif; font-size: 107%; margin: 0; } .prettyprint { border: 1px solid #ddd; width: 80%; overflow: auto; } .prettyprint.source { width: inherit; } .prettyprint code { font-family: Consolas, 'Lucida Console', Monaco, monospace; font-size: 100%; line-height: 18px; display: block; padding: 4px 12px; margin: 0; background-color: #fff; color: #000; } .prettyprint code span.line { display: inline-block; } .prettyprint.linenums { padding-left: 70px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .prettyprint.linenums ol { padding-left: 0; } .prettyprint.linenums li { border-left: 3px #ddd solid; } .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { background-color: lightyellow; } .prettyprint.linenums li * { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } .params, .props { border-spacing: 0; border: 0; border-collapse: collapse; } .params .name, .props .name, .name code { color: #526492; font-family: Consolas, 'Lucida Console', Monaco, monospace; font-size: 100%; } .params td, .params th, .props td, .props th { border: 1px solid #ddd; margin: 0px; text-align: left; vertical-align: top; padding: 4px 6px; display: table-cell; } .params thead tr, .props thead tr { background-color: #ddd; font-weight: bold; } .params .params thead tr, .props .props thead tr { background-color: #fff; font-weight: bold; } .params th, .props th { border-right: 1px solid #aaa; } .params thead .last, .props thead .last { border-right: 1px solid #ddd; } .params td.description > p:first-child { margin-top: 0; padding-top: 0; } .params td.description > p:last-child { margin-bottom: 0; padding-bottom: 0; } .disabled { color: #454545; } ================================================ FILE: doc/styles/prettify-jsdoc.css ================================================ /* JSDoc prettify.js theme */ /* plain text */ .pln { color: #000000; font-weight: normal; font-style: normal; } /* string content */ .str { color: #006400; font-weight: normal; font-style: normal; } /* a keyword */ .kwd { color: #000000; font-weight: bold; font-style: normal; } /* a comment */ .com { font-weight: normal; font-style: italic; } /* a type name */ .typ { color: #000000; font-weight: normal; font-style: normal; } /* a literal value */ .lit { color: #006400; font-weight: normal; font-style: normal; } /* punctuation */ .pun { color: #000000; font-weight: bold; font-style: normal; } /* lisp open bracket */ .opn { color: #000000; font-weight: bold; font-style: normal; } /* lisp close bracket */ .clo { color: #000000; font-weight: bold; font-style: normal; } /* a markup tag name */ .tag { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute name */ .atn { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute value */ .atv { color: #006400; font-weight: normal; font-style: normal; } /* a declaration */ .dec { color: #000000; font-weight: bold; font-style: normal; } /* a variable name */ .var { color: #000000; font-weight: normal; font-style: normal; } /* a function name */ .fun { color: #000000; font-weight: bold; font-style: normal; } /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } ================================================ FILE: doc/styles/prettify-tomorrow.css ================================================ /* Tomorrow Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* Pretty printing styles. Used with prettify.js. */ /* SPAN elements with the classes below are added by prettyprint. */ /* plain text */ .pln { color: #4d4d4c; } @media screen { /* string content */ .str { color: #718c00; } /* a keyword */ .kwd { color: #8959a8; } /* a comment */ .com { color: #8e908c; } /* a type name */ .typ { color: #4271ae; } /* a literal value */ .lit { color: #f5871f; } /* punctuation */ .pun { color: #4d4d4c; } /* lisp open bracket */ .opn { color: #4d4d4c; } /* lisp close bracket */ .clo { color: #4d4d4c; } /* a markup tag name */ .tag { color: #c82829; } /* a markup attribute name */ .atn { color: #f5871f; } /* a markup attribute value */ .atv { color: #3e999f; } /* a declaration */ .dec { color: #f5871f; } /* a variable name */ .var { color: #c82829; } /* a function name */ .fun { color: #4271ae; } } /* Use higher contrast and text-weight for printable form. */ @media print, projection { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun, .opn, .clo { color: #440; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } /* Style */ /* pre.prettyprint { background: white; font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; line-height: 1.5; border: 1px solid #ccc; padding: 10px; } */ /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } /* IE indents via margin-left */ li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { /* */ } /* Alternate shading for lines */ li.L1, li.L3, li.L5, li.L7, li.L9 { /* */ } ================================================ FILE: doc/utils.js.html ================================================ JSDoc: Source: utils.js

Source: utils.js

/*jshint node:true*/
'use strict';

var exec = require('child_process').exec;
var isWindows = require('os').platform().match(/win(32|64)/);
var which = require('which');

var nlRegexp = /\r\n|\r|\n/g;
var streamRegexp = /^\[?(.*?)\]?$/;
var filterEscapeRegexp = /[,]/;
var whichCache = {};

/**
 * Parse progress line from ffmpeg stderr
 *
 * @param {String} line progress line
 * @return progress object
 * @private
 */
function parseProgressLine(line) {
  var progress = {};

  // Remove all spaces after = and trim
  line  = line.replace(/=\s+/g, '=').trim();
  var progressParts = line.split(' ');

  // Split every progress part by "=" to get key and value
  for(var i = 0; i < progressParts.length; i++) {
    var progressSplit = progressParts[i].split('=', 2);
    var key = progressSplit[0];
    var value = progressSplit[1];

    // This is not a progress line
    if(typeof value === 'undefined')
      return null;

    progress[key] = value;
  }

  return progress;
}


var utils = module.exports = {
  isWindows: isWindows,
  streamRegexp: streamRegexp,


  /**
   * Copy an object keys into another one
   *
   * @param {Object} source source object
   * @param {Object} dest destination object
   * @private
   */
  copy: function(source, dest) {
    Object.keys(source).forEach(function(key) {
      dest[key] = source[key];
    });
  },


  /**
   * Create an argument list
   *
   * Returns a function that adds new arguments to the list.
   * It also has the following methods:
   * - clear() empties the argument list
   * - get() returns the argument list
   * - find(arg, count) finds 'arg' in the list and return the following 'count' items, or undefined if not found
   * - remove(arg, count) remove 'arg' in the list as well as the following 'count' items
   *
   * @private
   */
  args: function() {
    var list = [];

    // Append argument(s) to the list
    var argfunc = function() {
      if (arguments.length === 1 && Array.isArray(arguments[0])) {
        list = list.concat(arguments[0]);
      } else {
        list = list.concat([].slice.call(arguments));
      }
    };

    // Clear argument list
    argfunc.clear = function() {
      list = [];
    };

    // Return argument list
    argfunc.get = function() {
      return list;
    };

    // Find argument 'arg' in list, and if found, return an array of the 'count' items that follow it
    argfunc.find = function(arg, count) {
      var index = list.indexOf(arg);
      if (index !== -1) {
        return list.slice(index + 1, index + 1 + (count || 0));
      }
    };

    // Find argument 'arg' in list, and if found, remove it as well as the 'count' items that follow it
    argfunc.remove = function(arg, count) {
      var index = list.indexOf(arg);
      if (index !== -1) {
        list.splice(index, (count || 0) + 1);
      }
    };

    // Clone argument list
    argfunc.clone = function() {
      var cloned = utils.args();
      cloned(list);
      return cloned;
    };

    return argfunc;
  },


  /**
   * Generate filter strings
   *
   * @param {String[]|Object[]} filters filter specifications. When using objects,
   *   each must have the following properties:
   * @param {String} filters.filter filter name
   * @param {String|Array} [filters.inputs] (array of) input stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically choosing the first unused matching streams
   * @param {String|Array} [filters.outputs] (array of) output stream specifier(s) for the filter,
   *   defaults to ffmpeg automatically assigning the output to the output file
   * @param {Object|String|Array} [filters.options] filter options, can be omitted to not set any options
   * @return String[]
   * @private
   */
  makeFilterStrings: function(filters) {
    return filters.map(function(filterSpec) {
      if (typeof filterSpec === 'string') {
        return filterSpec;
      }

      var filterString = '';

      // Filter string format is:
      // [input1][input2]...filter[output1][output2]...
      // The 'filter' part can optionaly have arguments:
      //   filter=arg1:arg2:arg3
      //   filter=arg1=v1:arg2=v2:arg3=v3

      // Add inputs
      if (Array.isArray(filterSpec.inputs)) {
        filterString += filterSpec.inputs.map(function(streamSpec) {
          return streamSpec.replace(streamRegexp, '[$1]');
        }).join('');
      } else if (typeof filterSpec.inputs === 'string') {
        filterString += filterSpec.inputs.replace(streamRegexp, '[$1]');
      }

      // Add filter
      filterString += filterSpec.filter;

      // Add options
      if (filterSpec.options) {
        if (typeof filterSpec.options === 'string' || typeof filterSpec.options === 'number') {
          // Option string
          filterString += '=' + filterSpec.options;
        } else if (Array.isArray(filterSpec.options)) {
          // Option array (unnamed options)
          filterString += '=' + filterSpec.options.map(function(option) {
            if (typeof option === 'string' && option.match(filterEscapeRegexp)) {
              return '\'' + option + '\'';
            } else {
              return option;
            }
          }).join(':');
        } else if (Object.keys(filterSpec.options).length) {
          // Option object (named options)
          filterString += '=' + Object.keys(filterSpec.options).map(function(option) {
            var value = filterSpec.options[option];

            if (typeof value === 'string' && value.match(filterEscapeRegexp)) {
              value = '\'' + value + '\'';
            }

            return option + '=' + value;
          }).join(':');
        }
      }

      // Add outputs
      if (Array.isArray(filterSpec.outputs)) {
        filterString += filterSpec.outputs.map(function(streamSpec) {
          return streamSpec.replace(streamRegexp, '[$1]');
        }).join('');
      } else if (typeof filterSpec.outputs === 'string') {
        filterString += filterSpec.outputs.replace(streamRegexp, '[$1]');
      }

      return filterString;
    });
  },


  /**
   * Search for an executable
   *
   * Uses 'which' or 'where' depending on platform
   *
   * @param {String} name executable name
   * @param {Function} callback callback with signature (err, path)
   * @private
   */
  which: function(name, callback) {
    if (name in whichCache) {
      return callback(null, whichCache[name]);
    }

    which(name, function(err, result){
      if (err) {
        // Treat errors as not found
        return callback(null, whichCache[name] = '');
      }
      callback(null, whichCache[name] = result);
    });
  },


  /**
   * Convert a [[hh:]mm:]ss[.xxx] timemark into seconds
   *
   * @param {String} timemark timemark string
   * @return Number
   * @private
   */
  timemarkToSeconds: function(timemark) {
    if (typeof timemark === 'number') {
      return timemark;
    }

    if (timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) {
      return Number(timemark);
    }

    var parts = timemark.split(':');

    // add seconds
    var secs = Number(parts.pop());

    if (parts.length) {
      // add minutes
      secs += Number(parts.pop()) * 60;
    }

    if (parts.length) {
      // add hours
      secs += Number(parts.pop()) * 3600;
    }

    return secs;
  },


  /**
   * Extract codec data from ffmpeg stderr and emit 'codecData' event if appropriate
   * Call it with an initially empty codec object once with each line of stderr output until it returns true
   *
   * @param {FfmpegCommand} command event emitter
   * @param {String} stderrLine ffmpeg stderr output line
   * @param {Object} codecObject object used to accumulate codec data between calls
   * @return {Boolean} true if codec data is complete (and event was emitted), false otherwise
   * @private
   */
  extractCodecData: function(command, stderrLine, codecsObject) {
    var inputPattern = /Input #[0-9]+, ([^ ]+),/;
    var durPattern = /Duration\: ([^,]+)/;
    var audioPattern = /Audio\: (.*)/;
    var videoPattern = /Video\: (.*)/;

    if (!('inputStack' in codecsObject)) {
      codecsObject.inputStack = [];
      codecsObject.inputIndex = -1;
      codecsObject.inInput = false;
    }

    var inputStack = codecsObject.inputStack;
    var inputIndex = codecsObject.inputIndex;
    var inInput = codecsObject.inInput;

    var format, dur, audio, video;

    if (format = stderrLine.match(inputPattern)) {
      inInput = codecsObject.inInput = true;
      inputIndex = codecsObject.inputIndex = codecsObject.inputIndex + 1;

      inputStack[inputIndex] = { format: format[1], audio: '', video: '', duration: '' };
    } else if (inInput && (dur = stderrLine.match(durPattern))) {
      inputStack[inputIndex].duration = dur[1];
    } else if (inInput && (audio = stderrLine.match(audioPattern))) {
      audio = audio[1].split(', ');
      inputStack[inputIndex].audio = audio[0];
      inputStack[inputIndex].audio_details = audio;
    } else if (inInput && (video = stderrLine.match(videoPattern))) {
      video = video[1].split(', ');
      inputStack[inputIndex].video = video[0];
      inputStack[inputIndex].video_details = video;
    } else if (/Output #\d+/.test(stderrLine)) {
      inInput = codecsObject.inInput = false;
    } else if (/Stream mapping:|Press (\[q\]|ctrl-c) to stop/.test(stderrLine)) {
      command.emit.apply(command, ['codecData'].concat(inputStack));
      return true;
    }

    return false;
  },


  /**
   * Extract progress data from ffmpeg stderr and emit 'progress' event if appropriate
   *
   * @param {FfmpegCommand} command event emitter
   * @param {String} stderrLine ffmpeg stderr data
   * @param {Number} [duration=0] expected output duration in seconds
   * @private
   */
  extractProgress: function(command, stderrLine, duration) {
    var progress = parseProgressLine(stderrLine);

    if (progress) {
      // build progress report object
      var ret = {
        frames: parseInt(progress.frame, 10),
        currentFps: parseInt(progress.fps, 10),
        currentKbps: progress.bitrate ? parseFloat(progress.bitrate.replace('kbits/s', '')) : 0,
        targetSize: parseInt(progress.size, 10),
        timemark: progress.time
      };

      // calculate percent progress using duration
      if (duration && duration > 0) {
        ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100;
      }

      command.emit('progress', ret);
    }
  },


  /**
   * Extract error message(s) from ffmpeg stderr
   *
   * @param {String} stderr ffmpeg stderr data
   * @return {String}
   * @private
   */
  extractError: function(stderr) {
    // Only return the last stderr lines that don't start with a space or a square bracket
    return stderr.split(nlRegexp).reduce(function(messages, message) {
      if (message.charAt(0) === ' ' || message.charAt(0) === '[') {
        return [];
      } else {
        messages.push(message);
        return messages;
      }
    }, []).join('\n');
  },


  /**
   * Creates a line ring buffer object with the following methods:
   * - append(str) : appends a string or buffer
   * - get() : returns the whole string
   * - close() : prevents further append() calls and does a last call to callbacks
   * - callback(cb) : calls cb for each line (incl. those already in the ring)
   *
   * @param {Numebr} maxLines maximum number of lines to store (<= 0 for unlimited)
   */
  linesRing: function(maxLines) {
    var cbs = [];
    var lines = [];
    var current = null;
    var closed = false
    var max = maxLines - 1;

    function emit(line) {
      cbs.forEach(function(cb) { cb(line); });
    }

    return {
      callback: function(cb) {
        lines.forEach(function(l) { cb(l); });
        cbs.push(cb);
      },

      append: function(str) {
        if (closed) return;
        if (str instanceof Buffer) str = '' + str;
        if (!str || str.length === 0) return;

        var newLines = str.split(nlRegexp);

        if (newLines.length === 1) {
          if (current !== null) {
            current = current + newLines.shift();
          } else {
            current = newLines.shift();
          }
        } else {
          if (current !== null) {
            current = current + newLines.shift();
            emit(current);
            lines.push(current);
          }

          current = newLines.pop();

          newLines.forEach(function(l) {
            emit(l);
            lines.push(l);
          });

          if (max > -1 && lines.length > max) {
            lines.splice(0, lines.length - max);
          }
        }
      },

      get: function() {
        if (current !== null) {
          return lines.concat([current]).join('\n');
        } else {
          return lines.join('\n');
        }
      },

      close: function() {
        if (closed) return;

        if (current !== null) {
          emit(current);
          lines.push(current);

          if (max > -1 && lines.length > max) {
            lines.shift();
          }

          current = null;
        }

        closed = true;
      }
    };
  }
};

Documentation generated by JSDoc 3.4.0 on Sun May 01 2016 12:10:37 GMT+0200 (CEST)
================================================ FILE: doc/video.js.html ================================================ JSDoc: Source: options/video.js

Source: options/video.js

/*jshint node:true*/
'use strict';

var utils = require('../utils');


/*
 *! Video-related methods
 */

module.exports = function(proto) {
  /**
   * Disable video in the output
   *
   * @method FfmpegCommand#noVideo
   * @category Video
   * @aliases withNoVideo
   *
   * @return FfmpegCommand
   */
  proto.withNoVideo =
  proto.noVideo = function() {
    this._currentOutput.video.clear();
    this._currentOutput.videoFilters.clear();
    this._currentOutput.video('-vn');

    return this;
  };


  /**
   * Specify video codec
   *
   * @method FfmpegCommand#videoCodec
   * @category Video
   * @aliases withVideoCodec
   *
   * @param {String} codec video codec name
   * @return FfmpegCommand
   */
  proto.withVideoCodec =
  proto.videoCodec = function(codec) {
    this._currentOutput.video('-vcodec', codec);
    return this;
  };


  /**
   * Specify video bitrate
   *
   * @method FfmpegCommand#videoBitrate
   * @category Video
   * @aliases withVideoBitrate
   *
   * @param {String|Number} bitrate video bitrate in kbps (with an optional 'k' suffix)
   * @param {Boolean} [constant=false] enforce constant bitrate
   * @return FfmpegCommand
   */
  proto.withVideoBitrate =
  proto.videoBitrate = function(bitrate, constant) {
    bitrate = ('' + bitrate).replace(/k?$/, 'k');

    this._currentOutput.video('-b:v', bitrate);
    if (constant) {
      this._currentOutput.video(
        '-maxrate', bitrate,
        '-minrate', bitrate,
        '-bufsize', '3M'
      );
    }

    return this;
  };


  /**
   * Specify custom video filter(s)
   *
   * Can be called both with one or many filters, or a filter array.
   *
   * @example
   * command.videoFilters('filter1');
   *
   * @example
   * command.videoFilters('filter1', 'filter2=param1=value1:param2=value2');
   *
   * @example
   * command.videoFilters(['filter1', 'filter2']);
   *
   * @example
   * command.videoFilters([
   *   {
   *     filter: 'filter1'
   *   },
   *   {
   *     filter: 'filter2',
   *     options: 'param=value:param=value'
   *   }
   * ]);
   *
   * @example
   * command.videoFilters(
   *   {
   *     filter: 'filter1',
   *     options: ['value1', 'value2']
   *   },
   *   {
   *     filter: 'filter2',
   *     options: { param1: 'value1', param2: 'value2' }
   *   }
   * );
   *
   * @method FfmpegCommand#videoFilters
   * @category Video
   * @aliases withVideoFilter,withVideoFilters,videoFilter
   *
   * @param {...String|String[]|Object[]} filters video filter strings, string array or
   *   filter specification array, each with the following properties:
   * @param {String} filters.filter filter name
   * @param {String|String[]|Object} [filters.options] filter option string, array, or object
   * @return FfmpegCommand
   */
  proto.withVideoFilter =
  proto.withVideoFilters =
  proto.videoFilter =
  proto.videoFilters = function(filters) {
    if (arguments.length > 1) {
      filters = [].slice.call(arguments);
    }

    if (!Array.isArray(filters)) {
      filters = [filters];
    }

    this._currentOutput.videoFilters(utils.makeFilterStrings(filters));

    return this;
  };


  /**
   * Specify output FPS
   *
   * @method FfmpegCommand#fps
   * @category Video
   * @aliases withOutputFps,withOutputFPS,withFpsOutput,withFPSOutput,withFps,withFPS,outputFPS,outputFps,fpsOutput,FPSOutput,FPS
   *
   * @param {Number} fps output FPS
   * @return FfmpegCommand
   */
  proto.withOutputFps =
  proto.withOutputFPS =
  proto.withFpsOutput =
  proto.withFPSOutput =
  proto.withFps =
  proto.withFPS =
  proto.outputFPS =
  proto.outputFps =
  proto.fpsOutput =
  proto.FPSOutput =
  proto.fps =
  proto.FPS = function(fps) {
    this._currentOutput.video('-r', fps);
    return this;
  };


  /**
   * Only transcode a certain number of frames
   *
   * @method FfmpegCommand#frames
   * @category Video
   * @aliases takeFrames,withFrames
   *
   * @param {Number} frames frame count
   * @return FfmpegCommand
   */
  proto.takeFrames =
  proto.withFrames =
  proto.frames = function(frames) {
    this._currentOutput.video('-vframes', frames);
    return this;
  };
};

Documentation generated by JSDoc 3.3.0-alpha5 on Tue Jul 08 2014 21:22:19 GMT+0200 (CEST)
================================================ FILE: doc/videosize.js.html ================================================ JSDoc: Source: options/videosize.js

Source: options/videosize.js

/*jshint node:true*/
'use strict';

/*
 *! Size helpers
 */


/**
 * Return filters to pad video to width*height,
 *
 * @param {Number} width output width
 * @param {Number} height output height
 * @param {Number} aspect video aspect ratio (without padding)
 * @param {Number} color padding color
 * @return scale/pad filters
 * @private
 */
function getScalePadFilters(width, height, aspect, color) {
  /*
    let a be the input aspect ratio, A be the requested aspect ratio

    if a > A, padding is done on top and bottom
    if a < A, padding is done on left and right
   */

  return [
    /*
      In both cases, we first have to scale the input to match the requested size.
      When using computed width/height, we truncate them to multiples of 2
     */
    {
      filter: 'scale',
      options: {
        w: 'if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2)',
        h: 'if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)'
      }
    },

    /*
      Then we pad the scaled input to match the target size
      (here iw and ih refer to the padding input, i.e the scaled output)
     */

    {
      filter: 'pad',
      options: {
        w: width,
        h: height,
        x: 'if(gt(a,' + aspect + '),0,(' + width + '-iw)/2)',
        y: 'if(lt(a,' + aspect + '),0,(' + height + '-ih)/2)',
        color: color
      }
    }
  ];
}


/**
 * Recompute size filters
 *
 * @param {Object} output
 * @param {String} key newly-added parameter name ('size', 'aspect' or 'pad')
 * @param {String} value newly-added parameter value
 * @return filter string array
 * @private
 */
function createSizeFilters(output, key, value) {
  // Store parameters
  var data = output.sizeData = output.sizeData || {};
  data[key] = value;

  if (!('size' in data)) {
    // No size requested, keep original size
    return [];
  }

  // Try to match the different size string formats
  var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/);
  var fixedWidth = data.size.match(/([0-9]+)x\?/);
  var fixedHeight = data.size.match(/\?x([0-9]+)/);
  var percentRatio = data.size.match(/\b([0-9]{1,3})%/);
  var width, height, aspect;

  if (percentRatio) {
    var ratio = Number(percentRatio[1]) / 100;
    return [{
      filter: 'scale',
      options: {
        w: 'trunc(iw*' + ratio + '/2)*2',
        h: 'trunc(ih*' + ratio + '/2)*2'
      }
    }];
  } else if (fixedSize) {
    // Round target size to multiples of 2
    width = Math.round(Number(fixedSize[1]) / 2) * 2;
    height = Math.round(Number(fixedSize[2]) / 2) * 2;

    aspect = width / height;

    if (data.pad) {
      return getScalePadFilters(width, height, aspect, data.pad);
    } else {
      // No autopad requested, rescale to target size
      return [{ filter: 'scale', options: { w: width, h: height }}];
    }
  } else if (fixedWidth || fixedHeight) {
    if ('aspect' in data) {
      // Specified aspect ratio
      width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect);
      height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect);

      // Round to multiples of 2
      width = Math.round(width / 2) * 2;
      height = Math.round(height / 2) * 2;

      if (data.pad) {
        return getScalePadFilters(width, height, data.aspect, data.pad);
      } else {
        // No autopad requested, rescale to target size
        return [{ filter: 'scale', options: { w: width, h: height }}];
      }
    } else {
      // Keep input aspect ratio

      if (fixedWidth) {
        return [{
          filter: 'scale',
          options: {
            w: Math.round(Number(fixedWidth[1]) / 2) * 2,
            h: 'trunc(ow/a/2)*2'
          }
        }];
      } else {
        return [{
          filter: 'scale',
          options: {
            w: 'trunc(oh*a/2)*2',
            h: Math.round(Number(fixedHeight[1]) / 2) * 2
          }
        }];
      }
    }
  } else {
    throw new Error('Invalid size specified: ' + data.size);
  }
}


/*
 *! Video size-related methods
 */

module.exports = function(proto) {
  /**
   * Keep display aspect ratio
   *
   * This method is useful when converting an input with non-square pixels to an output format
   * that does not support non-square pixels.  It rescales the input so that the display aspect
   * ratio is the same.
   *
   * @method FfmpegCommand#keepDAR
   * @category Video size
   * @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio
   *
   * @return FfmpegCommand
   */
  proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio
  proto.keepDisplayAspect =
  proto.keepDisplayAspectRatio =
  proto.keepDAR = function() {
    return this.videoFilters([
      {
        filter: 'scale',
        options: {
          w: 'if(gt(sar,1),iw*sar,iw)',
          h: 'if(lt(sar,1),ih/sar,ih)'
        }
      },
      {
        filter: 'setsar',
        options: '1'
      }
    ]);
  };


  /**
   * Set output size
   *
   * The 'size' parameter can have one of 4 forms:
   * - 'X%': rescale to xx % of the original size
   * - 'WxH': specify width and height
   * - 'Wx?': specify width and compute height from input aspect ratio
   * - '?xH': specify height and compute width from input aspect ratio
   *
   * Note: both dimensions will be truncated to multiples of 2.
   *
   * @method FfmpegCommand#size
   * @category Video size
   * @aliases withSize,setSize
   *
   * @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240'
   * @return FfmpegCommand
   */
  proto.withSize =
  proto.setSize =
  proto.size = function(size) {
    var filters = createSizeFilters(this._currentOutput, 'size', size);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };


  /**
   * Set output aspect ratio
   *
   * @method FfmpegCommand#aspect
   * @category Video size
   * @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio
   *
   * @param {String|Number} aspect aspect ratio (number or 'X:Y' string)
   * @return FfmpegCommand
   */
  proto.withAspect =
  proto.withAspectRatio =
  proto.setAspect =
  proto.setAspectRatio =
  proto.aspect =
  proto.aspectRatio = function(aspect) {
    var a = Number(aspect);
    if (isNaN(a)) {
      var match = aspect.match(/^(\d+):(\d+)$/);
      if (match) {
        a = Number(match[1]) / Number(match[2]);
      } else {
        throw new Error('Invalid aspect ratio: ' + aspect);
      }
    }

    var filters = createSizeFilters(this._currentOutput, 'aspect', a);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };


  /**
   * Enable auto-padding the output
   *
   * @method FfmpegCommand#autopad
   * @category Video size
   * @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad
   *
   * @param {Boolean} [pad=true] enable/disable auto-padding
   * @param {String} [color='black'] pad color
   */
  proto.applyAutopadding =
  proto.applyAutoPadding =
  proto.applyAutopad =
  proto.applyAutoPad =
  proto.withAutopadding =
  proto.withAutoPadding =
  proto.withAutopad =
  proto.withAutoPad =
  proto.autoPad =
  proto.autopad = function(pad, color) {
    // Allow autopad(color)
    if (typeof pad === 'string') {
      color = pad;
      pad = true;
    }

    // Allow autopad() and autopad(undefined, color)
    if (typeof pad === 'undefined') {
      pad = true;
    }

    var filters = createSizeFilters(this._currentOutput, 'pad', pad ? color || 'black' : false);

    this._currentOutput.sizeFilters.clear();
    this._currentOutput.sizeFilters(filters);

    return this;
  };
};

Documentation generated by JSDoc 3.3.0-alpha5 on Tue Jul 08 2014 21:22:19 GMT+0200 (CEST)
================================================ FILE: examples/any-to-mp4-steam.js ================================================ // The solution based on adding -movflags for mp4 output // For more movflags details check ffmpeg docs // https://ffmpeg.org/ffmpeg-formats.html#toc-Options-9 var fs = require('fs'); var path = require('path'); var ffmpeg = require('../index'); var pathToSourceFile = path.resolve(__dirname, '../test/assets/testvideo-169.avi'); var readStream = fs.createReadStream(pathToSourceFile); var writeStream = fs.createWriteStream('./output.mp4'); ffmpeg(readStream) .addOutputOptions('-movflags +frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov') .format('mp4') .pipe(writeStream); ================================================ FILE: examples/express-stream.js ================================================ var express = require('express'), ffmpeg = require('../index'); var app = express(); app.use(express.static(__dirname + '/flowplayer')); app.get('/', function(req, res) { res.send('index.html'); }); app.get('/video/:filename', function(req, res) { res.contentType('flv'); // make sure you set the correct path to your video file storage var pathToMovie = '/path/to/storage/' + req.params.filename; var proc = ffmpeg(pathToMovie) // use the 'flashvideo' preset (located in /lib/presets/flashvideo.js) .preset('flashvideo') // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to stream .pipe(res, {end:true}); }); app.listen(4000); ================================================ FILE: examples/flowplayer/index.html ================================================ node-fluent-ffmpeg ================================================ FILE: examples/full.js ================================================ var ffmpeg = require('../index'); // make sure you set the correct path to your video file var proc = ffmpeg('/path/to/your_movie.avi') // set video bitrate .videoBitrate(1024) // set target codec .videoCodec('divx') // set aspect ratio .aspect('16:9') // set size in percent .size('50%') // set fps .fps(24) // set audio bitrate .audioBitrate('128k') // set audio codec .audioCodec('libmp3lame') // set number of audio channels .audioChannels(2) // set custom option .addOption('-vtag', 'DIVX') // set output format to force .format('avi') // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to file .save('/path/to/your_target.avi'); ================================================ FILE: examples/image2video.js ================================================ var ffmpeg = require('fluent-ffmpeg'); // make sure you set the correct path to your video file var proc = ffmpeg('/path/to/your_image.jpg') // loop for 5 seconds .loop(5) // using 25 fps .fps(25) // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to file .save('/path/to/your_target.m4v'); ================================================ FILE: examples/input-stream.js ================================================ var fs = require('fs'), ffmpeg = require('../index'); // open input stream var infs = fs.createReadStream(__dirname + '/test/assets/testvideo-43.avi'); infs.on('error', function(err) { console.log(err); }); // create new ffmpeg processor instance using input stream // instead of file path (can be any ReadableStream) var proc = ffmpeg(infs) .preset('flashvideo') // setup event handlers .on('end', function() { console.log('done processing input stream'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to file .save('/path/to/your_target.flv'); ================================================ FILE: examples/livertmp2hls.js ================================================ var ffmpeg = require('../index'); // make sure you set the correct path to your video file var proc = ffmpeg('rtmp://path/to/live/stream', { timeout: 432000 }) // set video bitrate .videoBitrate(1024) // set h264 preset .addOption('preset','superfast') // set target codec .videoCodec('libx264') // set audio bitrate .audioBitrate('128k') // set audio codec .audioCodec('libfaac') // set number of audio channels .audioChannels(2) // set hls segments time .addOption('-hls_time', 10) // include all the segments in the list .addOption('-hls_list_size',0) // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to file .save('/path/to/your_target.m3u8'); ================================================ FILE: examples/mergeVideos.js ================================================ var ffmpeg = require('../index'); /* replicates this sequence of commands: ffmpeg -i title.mp4 -qscale:v 1 intermediate1.mpg ffmpeg -i source.mp4 -qscale:v 1 intermediate2.mpg ffmpeg -i concat:"intermediate1.mpg|intermediate2.mpg" -c copy intermediate_all.mpg ffmpeg -i intermediate_all.mpg -qscale:v 2 output.mp4 Create temporary .mpg files for each video and deletes them after merge is completed. These files are created by filename pattern like [videoFilename.ext].temp.mpg [outputFilename.ext].temp.merged.mp4 */ var firstFile = "title.mp4"; var secondFile = "source.mp4"; var thirdFile = "third.mov"; var outPath = "out.mp4"; var proc = ffmpeg(firstFile) .input(secondFile) .input(thirdFile) //.input(fourthFile) //.input(...) .on('end', function() { console.log('files have been merged succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) .mergeToFile(outPath); ================================================ FILE: examples/metadata.js ================================================ var ffmpeg = require('../index'); // make sure you set the correct path to your video file ffmpeg.ffprobe('/path/to/your_movie.avi',function(err, metadata) { console.log(require('util').inspect(metadata, false, null)); }); ================================================ FILE: examples/preset.js ================================================ var ffmpeg = require('../index'); // make sure you set the correct path to your video file var proc = ffmpeg('/path/to/your_movie.avi') // use the 'podcast' preset (located in /lib/presets/podcast.js) .preset('podcast') // in case you want to override the preset's setting, just keep chaining .videoBitrate('512k') // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to file .save('/path/to/your_target.m4v'); ================================================ FILE: examples/progress.js ================================================ var fs = require('fs'), ffmpeg = require('../index'); // open input stream var infs = fs.createReadStream(__dirname + '/test/assets/testvideo-43.avi'); infs.on('error', function(err) { console.log(err); }); var proc = ffmpeg(infs) .preset('flashvideo') // setup event handlers .on('progress', function(info) { console.log('progress ' + info.percent + '%'); }) .on('end', function() { console.log('done processing input stream'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) .save('/path/to/your_target.flv'); ================================================ FILE: examples/stream.js ================================================ var ffmpeg = require('../index'), fs = require('fs'); // create the target stream (can be any WritableStream) var stream = fs.createWriteStream('/path/to/yout_target.flv') // make sure you set the correct path to your video file var proc = ffmpeg('/path/to/your_movie.avi') // use the 'flashvideo' preset (located in /lib/presets/flashvideo.js) .preset('flashvideo') // setup event handlers .on('end', function() { console.log('file has been converted succesfully'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // save to stream .pipe(stream, {end:true}); //end = true, close output stream after writing ================================================ FILE: examples/thumbnails.js ================================================ var ffmpeg = require('../index'); var proc = ffmpeg('/path/to/your_movie.avi') // setup event handlers .on('filenames', function(filenames) { console.log('screenshots are ' + filenames.join(', ')); }) .on('end', function() { console.log('screenshots were saved'); }) .on('error', function(err) { console.log('an error happened: ' + err.message); }) // take 2 screenshots at predefined timemarks and size .takeScreenshots({ count: 2, timemarks: [ '00:00:02.000', '6' ], size: '150x100' }, '/path/to/thumbnail/folder'); ================================================ FILE: index.js ================================================ module.exports = require('./lib/fluent-ffmpeg'); ================================================ FILE: lib/capabilities.js ================================================ /*jshint node:true*/ 'use strict'; var fs = require('fs'); var path = require('path'); var async = require('async'); var utils = require('./utils'); /* *! Capability helpers */ var avCodecRegexp = /^\s*([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/; var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/; var ffEncodersRegexp = /\(encoders:([^\)]+)\)/; var ffDecodersRegexp = /\(decoders:([^\)]+)\)/; var encodersRegexp = /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/; var formatRegexp = /^\s*([D ])([E ])\s+([^ ]+)\s+(.*)$/; var lineBreakRegexp = /\r\n|\r|\n/; var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/; var cache = {}; module.exports = function(proto) { /** * Manually define the ffmpeg binary full path. * * @method FfmpegCommand#setFfmpegPath * * @param {String} ffmpegPath The full path to the ffmpeg binary. * @return FfmpegCommand */ proto.setFfmpegPath = function(ffmpegPath) { cache.ffmpegPath = ffmpegPath; return this; }; /** * Manually define the ffprobe binary full path. * * @method FfmpegCommand#setFfprobePath * * @param {String} ffprobePath The full path to the ffprobe binary. * @return FfmpegCommand */ proto.setFfprobePath = function(ffprobePath) { cache.ffprobePath = ffprobePath; return this; }; /** * Manually define the flvtool2/flvmeta binary full path. * * @method FfmpegCommand#setFlvtoolPath * * @param {String} flvtool The full path to the flvtool2 or flvmeta binary. * @return FfmpegCommand */ proto.setFlvtoolPath = function(flvtool) { cache.flvtoolPath = flvtool; return this; }; /** * Forget executable paths * * (only used for testing purposes) * * @method FfmpegCommand#_forgetPaths * @private */ proto._forgetPaths = function() { delete cache.ffmpegPath; delete cache.ffprobePath; delete cache.flvtoolPath; }; /** * Check for ffmpeg availability * * If the FFMPEG_PATH environment variable is set, try to use it. * If it is unset or incorrect, try to find ffmpeg in the PATH instead. * * @method FfmpegCommand#_getFfmpegPath * @param {Function} callback callback with signature (err, path) * @private */ proto._getFfmpegPath = function(callback) { if ('ffmpegPath' in cache) { return callback(null, cache.ffmpegPath); } async.waterfall([ // Try FFMPEG_PATH function(cb) { if (process.env.FFMPEG_PATH) { fs.exists(process.env.FFMPEG_PATH, function(exists) { if (exists) { cb(null, process.env.FFMPEG_PATH); } else { cb(null, ''); } }); } else { cb(null, ''); } }, // Search in the PATH function(ffmpeg, cb) { if (ffmpeg.length) { return cb(null, ffmpeg); } utils.which('ffmpeg', function(err, ffmpeg) { cb(err, ffmpeg); }); } ], function(err, ffmpeg) { if (err) { callback(err); } else { callback(null, cache.ffmpegPath = (ffmpeg || '')); } }); }; /** * Check for ffprobe availability * * If the FFPROBE_PATH environment variable is set, try to use it. * If it is unset or incorrect, try to find ffprobe in the PATH instead. * If this still fails, try to find ffprobe in the same directory as ffmpeg. * * @method FfmpegCommand#_getFfprobePath * @param {Function} callback callback with signature (err, path) * @private */ proto._getFfprobePath = function(callback) { var self = this; if ('ffprobePath' in cache) { return callback(null, cache.ffprobePath); } async.waterfall([ // Try FFPROBE_PATH function(cb) { if (process.env.FFPROBE_PATH) { fs.exists(process.env.FFPROBE_PATH, function(exists) { cb(null, exists ? process.env.FFPROBE_PATH : ''); }); } else { cb(null, ''); } }, // Search in the PATH function(ffprobe, cb) { if (ffprobe.length) { return cb(null, ffprobe); } utils.which('ffprobe', function(err, ffprobe) { cb(err, ffprobe); }); }, // Search in the same directory as ffmpeg function(ffprobe, cb) { if (ffprobe.length) { return cb(null, ffprobe); } self._getFfmpegPath(function(err, ffmpeg) { if (err) { cb(err); } else if (ffmpeg.length) { var name = utils.isWindows ? 'ffprobe.exe' : 'ffprobe'; var ffprobe = path.join(path.dirname(ffmpeg), name); fs.exists(ffprobe, function(exists) { cb(null, exists ? ffprobe : ''); }); } else { cb(null, ''); } }); } ], function(err, ffprobe) { if (err) { callback(err); } else { callback(null, cache.ffprobePath = (ffprobe || '')); } }); }; /** * Check for flvtool2/flvmeta availability * * If the FLVTOOL2_PATH or FLVMETA_PATH environment variable are set, try to use them. * If both are either unset or incorrect, try to find flvtool2 or flvmeta in the PATH instead. * * @method FfmpegCommand#_getFlvtoolPath * @param {Function} callback callback with signature (err, path) * @private */ proto._getFlvtoolPath = function(callback) { if ('flvtoolPath' in cache) { return callback(null, cache.flvtoolPath); } async.waterfall([ // Try FLVMETA_PATH function(cb) { if (process.env.FLVMETA_PATH) { fs.exists(process.env.FLVMETA_PATH, function(exists) { cb(null, exists ? process.env.FLVMETA_PATH : ''); }); } else { cb(null, ''); } }, // Try FLVTOOL2_PATH function(flvtool, cb) { if (flvtool.length) { return cb(null, flvtool); } if (process.env.FLVTOOL2_PATH) { fs.exists(process.env.FLVTOOL2_PATH, function(exists) { cb(null, exists ? process.env.FLVTOOL2_PATH : ''); }); } else { cb(null, ''); } }, // Search for flvmeta in the PATH function(flvtool, cb) { if (flvtool.length) { return cb(null, flvtool); } utils.which('flvmeta', function(err, flvmeta) { cb(err, flvmeta); }); }, // Search for flvtool2 in the PATH function(flvtool, cb) { if (flvtool.length) { return cb(null, flvtool); } utils.which('flvtool2', function(err, flvtool2) { cb(err, flvtool2); }); }, ], function(err, flvtool) { if (err) { callback(err); } else { callback(null, cache.flvtoolPath = (flvtool || '')); } }); }; /** * A callback passed to {@link FfmpegCommand#availableFilters}. * * @callback FfmpegCommand~filterCallback * @param {Error|null} err error object or null if no error happened * @param {Object} filters filter object with filter names as keys and the following * properties for each filter: * @param {String} filters.description filter description * @param {String} filters.input input type, one of 'audio', 'video' and 'none' * @param {Boolean} filters.multipleInputs whether the filter supports multiple inputs * @param {String} filters.output output type, one of 'audio', 'video' and 'none' * @param {Boolean} filters.multipleOutputs whether the filter supports multiple outputs */ /** * Query ffmpeg for available filters * * @method FfmpegCommand#availableFilters * @category Capabilities * @aliases getAvailableFilters * * @param {FfmpegCommand~filterCallback} callback callback function */ proto.availableFilters = proto.getAvailableFilters = function(callback) { if ('filters' in cache) { return callback(null, cache.filters); } this._spawnFfmpeg(['-filters'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) { if (err) { return callback(err); } var stdout = stdoutRing.get(); var lines = stdout.split('\n'); var data = {}; var types = { A: 'audio', V: 'video', '|': 'none' }; lines.forEach(function(line) { var match = line.match(filterRegexp); if (match) { data[match[1]] = { description: match[4], input: types[match[2].charAt(0)], multipleInputs: match[2].length > 1, output: types[match[3].charAt(0)], multipleOutputs: match[3].length > 1 }; } }); callback(null, cache.filters = data); }); }; /** * A callback passed to {@link FfmpegCommand#availableCodecs}. * * @callback FfmpegCommand~codecCallback * @param {Error|null} err error object or null if no error happened * @param {Object} codecs codec object with codec names as keys and the following * properties for each codec (more properties may be available depending on the * ffmpeg version used): * @param {String} codecs.description codec description * @param {Boolean} codecs.canDecode whether the codec is able to decode streams * @param {Boolean} codecs.canEncode whether the codec is able to encode streams */ /** * Query ffmpeg for available codecs * * @method FfmpegCommand#availableCodecs * @category Capabilities * @aliases getAvailableCodecs * * @param {FfmpegCommand~codecCallback} callback callback function */ proto.availableCodecs = proto.getAvailableCodecs = function(callback) { if ('codecs' in cache) { return callback(null, cache.codecs); } this._spawnFfmpeg(['-codecs'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) { if (err) { return callback(err); } var stdout = stdoutRing.get(); var lines = stdout.split(lineBreakRegexp); var data = {}; lines.forEach(function(line) { var match = line.match(avCodecRegexp); if (match && match[7] !== '=') { data[match[7]] = { type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]], description: match[8], canDecode: match[1] === 'D', canEncode: match[2] === 'E', drawHorizBand: match[4] === 'S', directRendering: match[5] === 'D', weirdFrameTruncation: match[6] === 'T' }; } match = line.match(ffCodecRegexp); if (match && match[7] !== '=') { var codecData = data[match[7]] = { type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]], description: match[8], canDecode: match[1] === 'D', canEncode: match[2] === 'E', intraFrameOnly: match[4] === 'I', isLossy: match[5] === 'L', isLossless: match[6] === 'S' }; var encoders = codecData.description.match(ffEncodersRegexp); encoders = encoders ? encoders[1].trim().split(' ') : []; var decoders = codecData.description.match(ffDecodersRegexp); decoders = decoders ? decoders[1].trim().split(' ') : []; if (encoders.length || decoders.length) { var coderData = {}; utils.copy(codecData, coderData); delete coderData.canEncode; delete coderData.canDecode; encoders.forEach(function(name) { data[name] = {}; utils.copy(coderData, data[name]); data[name].canEncode = true; }); decoders.forEach(function(name) { if (name in data) { data[name].canDecode = true; } else { data[name] = {}; utils.copy(coderData, data[name]); data[name].canDecode = true; } }); } } }); callback(null, cache.codecs = data); }); }; /** * A callback passed to {@link FfmpegCommand#availableEncoders}. * * @callback FfmpegCommand~encodersCallback * @param {Error|null} err error object or null if no error happened * @param {Object} encoders encoders object with encoder names as keys and the following * properties for each encoder: * @param {String} encoders.description codec description * @param {Boolean} encoders.type "audio", "video" or "subtitle" * @param {Boolean} encoders.frameMT whether the encoder is able to do frame-level multithreading * @param {Boolean} encoders.sliceMT whether the encoder is able to do slice-level multithreading * @param {Boolean} encoders.experimental whether the encoder is experimental * @param {Boolean} encoders.drawHorizBand whether the encoder supports draw_horiz_band * @param {Boolean} encoders.directRendering whether the encoder supports direct encoding method 1 */ /** * Query ffmpeg for available encoders * * @method FfmpegCommand#availableEncoders * @category Capabilities * @aliases getAvailableEncoders * * @param {FfmpegCommand~encodersCallback} callback callback function */ proto.availableEncoders = proto.getAvailableEncoders = function(callback) { if ('encoders' in cache) { return callback(null, cache.encoders); } this._spawnFfmpeg(['-encoders'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) { if (err) { return callback(err); } var stdout = stdoutRing.get(); var lines = stdout.split(lineBreakRegexp); var data = {}; lines.forEach(function(line) { var match = line.match(encodersRegexp); if (match && match[7] !== '=') { data[match[7]] = { type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[1]], description: match[8], frameMT: match[2] === 'F', sliceMT: match[3] === 'S', experimental: match[4] === 'X', drawHorizBand: match[5] === 'B', directRendering: match[6] === 'D' }; } }); callback(null, cache.encoders = data); }); }; /** * A callback passed to {@link FfmpegCommand#availableFormats}. * * @callback FfmpegCommand~formatCallback * @param {Error|null} err error object or null if no error happened * @param {Object} formats format object with format names as keys and the following * properties for each format: * @param {String} formats.description format description * @param {Boolean} formats.canDemux whether the format is able to demux streams from an input file * @param {Boolean} formats.canMux whether the format is able to mux streams into an output file */ /** * Query ffmpeg for available formats * * @method FfmpegCommand#availableFormats * @category Capabilities * @aliases getAvailableFormats * * @param {FfmpegCommand~formatCallback} callback callback function */ proto.availableFormats = proto.getAvailableFormats = function(callback) { if ('formats' in cache) { return callback(null, cache.formats); } // Run ffmpeg -formats this._spawnFfmpeg(['-formats'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) { if (err) { return callback(err); } // Parse output var stdout = stdoutRing.get(); var lines = stdout.split(lineBreakRegexp); var data = {}; lines.forEach(function(line) { var match = line.match(formatRegexp); if (match) { match[3].split(',').forEach(function(format) { if (!(format in data)) { data[format] = { description: match[4], canDemux: false, canMux: false }; } if (match[1] === 'D') { data[format].canDemux = true; } if (match[2] === 'E') { data[format].canMux = true; } }); } }); callback(null, cache.formats = data); }); }; /** * Check capabilities before executing a command * * Checks whether all used codecs and formats are indeed available * * @method FfmpegCommand#_checkCapabilities * @param {Function} callback callback with signature (err) * @private */ proto._checkCapabilities = function(callback) { var self = this; async.waterfall([ // Get available formats function(cb) { self.availableFormats(cb); }, // Check whether specified formats are available function(formats, cb) { var unavailable; // Output format(s) unavailable = self._outputs .reduce(function(fmts, output) { var format = output.options.find('-f', 1); if (format) { if (!(format[0] in formats) || !(formats[format[0]].canMux)) { fmts.push(format); } } return fmts; }, []); if (unavailable.length === 1) { return cb(new Error('Output format ' + unavailable[0] + ' is not available')); } else if (unavailable.length > 1) { return cb(new Error('Output formats ' + unavailable.join(', ') + ' are not available')); } // Input format(s) unavailable = self._inputs .reduce(function(fmts, input) { var format = input.options.find('-f', 1); if (format) { if (!(format[0] in formats) || !(formats[format[0]].canDemux)) { fmts.push(format[0]); } } return fmts; }, []); if (unavailable.length === 1) { return cb(new Error('Input format ' + unavailable[0] + ' is not available')); } else if (unavailable.length > 1) { return cb(new Error('Input formats ' + unavailable.join(', ') + ' are not available')); } cb(); }, // Get available codecs function(cb) { self.availableEncoders(cb); }, // Check whether specified codecs are available and add strict experimental options if needed function(encoders, cb) { var unavailable; // Audio codec(s) unavailable = self._outputs.reduce(function(cdcs, output) { var acodec = output.audio.find('-acodec', 1); if (acodec && acodec[0] !== 'copy') { if (!(acodec[0] in encoders) || encoders[acodec[0]].type !== 'audio') { cdcs.push(acodec[0]); } } return cdcs; }, []); if (unavailable.length === 1) { return cb(new Error('Audio codec ' + unavailable[0] + ' is not available')); } else if (unavailable.length > 1) { return cb(new Error('Audio codecs ' + unavailable.join(', ') + ' are not available')); } // Video codec(s) unavailable = self._outputs.reduce(function(cdcs, output) { var vcodec = output.video.find('-vcodec', 1); if (vcodec && vcodec[0] !== 'copy') { if (!(vcodec[0] in encoders) || encoders[vcodec[0]].type !== 'video') { cdcs.push(vcodec[0]); } } return cdcs; }, []); if (unavailable.length === 1) { return cb(new Error('Video codec ' + unavailable[0] + ' is not available')); } else if (unavailable.length > 1) { return cb(new Error('Video codecs ' + unavailable.join(', ') + ' are not available')); } cb(); } ], callback); }; }; ================================================ FILE: lib/ffprobe.js ================================================ /*jshint node:true, laxcomma:true*/ 'use strict'; var spawn = require('child_process').spawn; function legacyTag(key) { return key.match(/^TAG:/); } function legacyDisposition(key) { return key.match(/^DISPOSITION:/); } function parseFfprobeOutput(out) { var lines = out.split(/\r\n|\r|\n/); lines = lines.filter(function (line) { return line.length > 0; }); var data = { streams: [], format: {}, chapters: [] }; function parseBlock(name) { var data = {}; var line = lines.shift(); while (typeof line !== 'undefined') { if (line.toLowerCase() == '[/'+name+']') { return data; } else if (line.match(/^\[/)) { line = lines.shift(); continue; } var kv = line.match(/^([^=]+)=(.*)$/); if (kv) { if (!(kv[1].match(/^TAG:/)) && kv[2].match(/^[0-9]+(\.[0-9]+)?$/)) { data[kv[1]] = Number(kv[2]); } else { data[kv[1]] = kv[2]; } } line = lines.shift(); } return data; } var line = lines.shift(); while (typeof line !== 'undefined') { if (line.match(/^\[stream/i)) { var stream = parseBlock('stream'); data.streams.push(stream); } else if (line.match(/^\[chapter/i)) { var chapter = parseBlock('chapter'); data.chapters.push(chapter); } else if (line.toLowerCase() === '[format]') { data.format = parseBlock('format'); } line = lines.shift(); } return data; } module.exports = function(proto) { /** * A callback passed to the {@link FfmpegCommand#ffprobe} method. * * @callback FfmpegCommand~ffprobeCallback * * @param {Error|null} err error object or null if no error happened * @param {Object} ffprobeData ffprobe output data; this object * has the same format as what the following command returns: * * `ffprobe -print_format json -show_streams -show_format INPUTFILE` * @param {Array} ffprobeData.streams stream information * @param {Object} ffprobeData.format format information */ /** * Run ffprobe on last specified input * * @method FfmpegCommand#ffprobe * @category Metadata * * @param {?Number} [index] 0-based index of input to probe (defaults to last input) * @param {?String[]} [options] array of output options to return * @param {FfmpegCommand~ffprobeCallback} callback callback function * */ proto.ffprobe = function() { var input, index = null, options = [], callback; // the last argument should be the callback var callback = arguments[arguments.length - 1]; var ended = false function handleCallback(err, data) { if (!ended) { ended = true; callback(err, data); } }; // map the arguments to the correct variable names switch (arguments.length) { case 3: index = arguments[0]; options = arguments[1]; break; case 2: if (typeof arguments[0] === 'number') { index = arguments[0]; } else if (Array.isArray(arguments[0])) { options = arguments[0]; } break; } if (index === null) { if (!this._currentInput) { return handleCallback(new Error('No input specified')); } input = this._currentInput; } else { input = this._inputs[index]; if (!input) { return handleCallback(new Error('Invalid input index')); } } // Find ffprobe this._getFfprobePath(function(err, path) { if (err) { return handleCallback(err); } else if (!path) { return handleCallback(new Error('Cannot find ffprobe')); } var stdout = ''; var stdoutClosed = false; var stderr = ''; var stderrClosed = false; // Spawn ffprobe var src = input.isStream ? 'pipe:0' : input.source; var ffprobe = spawn(path, ['-show_streams', '-show_format'].concat(options, src), {windowsHide: true}); if (input.isStream) { // Skip errors on stdin. These get thrown when ffprobe is complete and // there seems to be no way hook in and close stdin before it throws. ffprobe.stdin.on('error', function(err) { if (['ECONNRESET', 'EPIPE', 'EOF'].indexOf(err.code) >= 0) { return; } handleCallback(err); }); // Once ffprobe's input stream closes, we need no more data from the // input ffprobe.stdin.on('close', function() { input.source.pause(); input.source.unpipe(ffprobe.stdin); }); input.source.pipe(ffprobe.stdin); } ffprobe.on('error', callback); // Ensure we wait for captured streams to end before calling callback var exitError = null; function handleExit(err) { if (err) { exitError = err; } if (processExited && stdoutClosed && stderrClosed) { if (exitError) { if (stderr) { exitError.message += '\n' + stderr; } return handleCallback(exitError); } // Process output var data = parseFfprobeOutput(stdout); // Handle legacy output with "TAG:x" and "DISPOSITION:x" keys [data.format].concat(data.streams).forEach(function(target) { if (target) { var legacyTagKeys = Object.keys(target).filter(legacyTag); if (legacyTagKeys.length) { target.tags = target.tags || {}; legacyTagKeys.forEach(function(tagKey) { target.tags[tagKey.substr(4)] = target[tagKey]; delete target[tagKey]; }); } var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition); if (legacyDispositionKeys.length) { target.disposition = target.disposition || {}; legacyDispositionKeys.forEach(function(dispositionKey) { target.disposition[dispositionKey.substr(12)] = target[dispositionKey]; delete target[dispositionKey]; }); } } }); handleCallback(null, data); } } // Handle ffprobe exit var processExited = false; ffprobe.on('exit', function(code, signal) { processExited = true; if (code) { handleExit(new Error('ffprobe exited with code ' + code)); } else if (signal) { handleExit(new Error('ffprobe was killed with signal ' + signal)); } else { handleExit(); } }); // Handle stdout/stderr streams ffprobe.stdout.on('data', function(data) { stdout += data; }); ffprobe.stdout.on('close', function() { stdoutClosed = true; handleExit(); }); ffprobe.stderr.on('data', function(data) { stderr += data; }); ffprobe.stderr.on('close', function() { stderrClosed = true; handleExit(); }); }); }; }; ================================================ FILE: lib/fluent-ffmpeg.js ================================================ /*jshint node:true*/ 'use strict'; var path = require('path'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var ARGLISTS = ['_global', '_audio', '_audioFilters', '_video', '_videoFilters', '_sizeFilters', '_complexFilters']; /** * Create an ffmpeg command * * Can be called with or without the 'new' operator, and the 'input' parameter * may be specified as 'options.source' instead (or passed later with the * addInput method). * * @constructor * @param {String|ReadableStream} [input] input file path or readable stream * @param {Object} [options] command options * @param {Object} [options.logger=] logger object with 'error', 'warning', 'info' and 'debug' methods * @param {Number} [options.niceness=0] ffmpeg process niceness, ignored on Windows * @param {Number} [options.priority=0] alias for `niceness` * @param {String} [options.presets="fluent-ffmpeg/lib/presets"] directory to load presets from * @param {String} [options.preset="fluent-ffmpeg/lib/presets"] alias for `presets` * @param {String} [options.stdoutLines=100] maximum lines of ffmpeg output to keep in memory, use 0 for unlimited * @param {Number} [options.timeout=] ffmpeg processing timeout in seconds * @param {String|ReadableStream} [options.source=] alias for the `input` parameter */ function FfmpegCommand(input, options) { // Make 'new' optional if (!(this instanceof FfmpegCommand)) { return new FfmpegCommand(input, options); } EventEmitter.call(this); if (typeof input === 'object' && !('readable' in input)) { // Options object passed directly options = input; } else { // Input passed first options = options || {}; options.source = input; } // Add input if present this._inputs = []; if (options.source) { this.input(options.source); } // Add target-less output for backwards compatibility this._outputs = []; this.output(); // Create argument lists var self = this; ['_global', '_complexFilters'].forEach(function(prop) { self[prop] = utils.args(); }); // Set default option values options.stdoutLines = 'stdoutLines' in options ? options.stdoutLines : 100; options.presets = options.presets || options.preset || path.join(__dirname, 'presets'); options.niceness = options.niceness || options.priority || 0; // Save options this.options = options; // Setup logger this.logger = options.logger || { debug: function() {}, info: function() {}, warn: function() {}, error: function() {} }; } util.inherits(FfmpegCommand, EventEmitter); module.exports = FfmpegCommand; /** * Clone an ffmpeg command * * This method is useful when you want to process the same input multiple times. * It returns a new FfmpegCommand instance with the exact same options. * * All options set _after_ the clone() call will only be applied to the instance * it has been called on. * * @example * var command = ffmpeg('/path/to/source.avi') * .audioCodec('libfaac') * .videoCodec('libx264') * .format('mp4'); * * command.clone() * .size('320x200') * .save('/path/to/output-small.mp4'); * * command.clone() * .size('640x400') * .save('/path/to/output-medium.mp4'); * * command.save('/path/to/output-original-size.mp4'); * * @method FfmpegCommand#clone * @return FfmpegCommand */ FfmpegCommand.prototype.clone = function() { var clone = new FfmpegCommand(); var self = this; // Clone options and logger clone.options = this.options; clone.logger = this.logger; // Clone inputs clone._inputs = this._inputs.map(function(input) { return { source: input.source, options: input.options.clone() }; }); // Create first output if ('target' in this._outputs[0]) { // We have outputs set, don't clone them and create first output clone._outputs = []; clone.output(); } else { // No outputs set, clone first output options clone._outputs = [ clone._currentOutput = { flags: {} } ]; ['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) { clone._currentOutput[key] = self._currentOutput[key].clone(); }); if (this._currentOutput.sizeData) { clone._currentOutput.sizeData = {}; utils.copy(this._currentOutput.sizeData, clone._currentOutput.sizeData); } utils.copy(this._currentOutput.flags, clone._currentOutput.flags); } // Clone argument lists ['_global', '_complexFilters'].forEach(function(prop) { clone[prop] = self[prop].clone(); }); return clone; }; /* Add methods from options submodules */ require('./options/inputs')(FfmpegCommand.prototype); require('./options/audio')(FfmpegCommand.prototype); require('./options/video')(FfmpegCommand.prototype); require('./options/videosize')(FfmpegCommand.prototype); require('./options/output')(FfmpegCommand.prototype); require('./options/custom')(FfmpegCommand.prototype); require('./options/misc')(FfmpegCommand.prototype); /* Add processor methods */ require('./processor')(FfmpegCommand.prototype); /* Add capabilities methods */ require('./capabilities')(FfmpegCommand.prototype); FfmpegCommand.setFfmpegPath = function(path) { (new FfmpegCommand()).setFfmpegPath(path); }; FfmpegCommand.setFfprobePath = function(path) { (new FfmpegCommand()).setFfprobePath(path); }; FfmpegCommand.setFlvtoolPath = function(path) { (new FfmpegCommand()).setFlvtoolPath(path); }; FfmpegCommand.availableFilters = FfmpegCommand.getAvailableFilters = function(callback) { (new FfmpegCommand()).availableFilters(callback); }; FfmpegCommand.availableCodecs = FfmpegCommand.getAvailableCodecs = function(callback) { (new FfmpegCommand()).availableCodecs(callback); }; FfmpegCommand.availableFormats = FfmpegCommand.getAvailableFormats = function(callback) { (new FfmpegCommand()).availableFormats(callback); }; FfmpegCommand.availableEncoders = FfmpegCommand.getAvailableEncoders = function(callback) { (new FfmpegCommand()).availableEncoders(callback); }; /* Add ffprobe methods */ require('./ffprobe')(FfmpegCommand.prototype); FfmpegCommand.ffprobe = function(file) { var instance = new FfmpegCommand(file); instance.ffprobe.apply(instance, Array.prototype.slice.call(arguments, 1)); }; /* Add processing recipes */ require('./recipes')(FfmpegCommand.prototype); ================================================ FILE: lib/options/audio.js ================================================ /*jshint node:true*/ 'use strict'; var utils = require('../utils'); /* *! Audio-related methods */ module.exports = function(proto) { /** * Disable audio in the output * * @method FfmpegCommand#noAudio * @category Audio * @aliases withNoAudio * @return FfmpegCommand */ proto.withNoAudio = proto.noAudio = function() { this._currentOutput.audio.clear(); this._currentOutput.audioFilters.clear(); this._currentOutput.audio('-an'); return this; }; /** * Specify audio codec * * @method FfmpegCommand#audioCodec * @category Audio * @aliases withAudioCodec * * @param {String} codec audio codec name * @return FfmpegCommand */ proto.withAudioCodec = proto.audioCodec = function(codec) { this._currentOutput.audio('-acodec', codec); return this; }; /** * Specify audio bitrate * * @method FfmpegCommand#audioBitrate * @category Audio * @aliases withAudioBitrate * * @param {String|Number} bitrate audio bitrate in kbps (with an optional 'k' suffix) * @return FfmpegCommand */ proto.withAudioBitrate = proto.audioBitrate = function(bitrate) { this._currentOutput.audio('-b:a', ('' + bitrate).replace(/k?$/, 'k')); return this; }; /** * Specify audio channel count * * @method FfmpegCommand#audioChannels * @category Audio * @aliases withAudioChannels * * @param {Number} channels channel count * @return FfmpegCommand */ proto.withAudioChannels = proto.audioChannels = function(channels) { this._currentOutput.audio('-ac', channels); return this; }; /** * Specify audio frequency * * @method FfmpegCommand#audioFrequency * @category Audio * @aliases withAudioFrequency * * @param {Number} freq audio frequency in Hz * @return FfmpegCommand */ proto.withAudioFrequency = proto.audioFrequency = function(freq) { this._currentOutput.audio('-ar', freq); return this; }; /** * Specify audio quality * * @method FfmpegCommand#audioQuality * @category Audio * @aliases withAudioQuality * * @param {Number} quality audio quality factor * @return FfmpegCommand */ proto.withAudioQuality = proto.audioQuality = function(quality) { this._currentOutput.audio('-aq', quality); return this; }; /** * Specify custom audio filter(s) * * Can be called both with one or many filters, or a filter array. * * @example * command.audioFilters('filter1'); * * @example * command.audioFilters('filter1', 'filter2=param1=value1:param2=value2'); * * @example * command.audioFilters(['filter1', 'filter2']); * * @example * command.audioFilters([ * { * filter: 'filter1' * }, * { * filter: 'filter2', * options: 'param=value:param=value' * } * ]); * * @example * command.audioFilters( * { * filter: 'filter1', * options: ['value1', 'value2'] * }, * { * filter: 'filter2', * options: { param1: 'value1', param2: 'value2' } * } * ); * * @method FfmpegCommand#audioFilters * @aliases withAudioFilter,withAudioFilters,audioFilter * @category Audio * * @param {...String|String[]|Object[]} filters audio filter strings, string array or * filter specification array, each with the following properties: * @param {String} filters.filter filter name * @param {String|String[]|Object} [filters.options] filter option string, array, or object * @return FfmpegCommand */ proto.withAudioFilter = proto.withAudioFilters = proto.audioFilter = proto.audioFilters = function(filters) { if (arguments.length > 1) { filters = [].slice.call(arguments); } if (!Array.isArray(filters)) { filters = [filters]; } this._currentOutput.audioFilters(utils.makeFilterStrings(filters)); return this; }; }; ================================================ FILE: lib/options/custom.js ================================================ /*jshint node:true*/ 'use strict'; var utils = require('../utils'); /* *! Custom options methods */ module.exports = function(proto) { /** * Add custom input option(s) * * When passing a single string or an array, each string containing two * words is split (eg. inputOptions('-option value') is supported) for * compatibility reasons. This is not the case when passing more than * one argument. * * @example * command.inputOptions('option1'); * * @example * command.inputOptions('option1', 'option2'); * * @example * command.inputOptions(['option1', 'option2']); * * @method FfmpegCommand#inputOptions * @category Custom options * @aliases addInputOption,addInputOptions,withInputOption,withInputOptions,inputOption * * @param {...String} options option string(s) or string array * @return FfmpegCommand */ proto.addInputOption = proto.addInputOptions = proto.withInputOption = proto.withInputOptions = proto.inputOption = proto.inputOptions = function(options) { if (!this._currentInput) { throw new Error('No input specified'); } var doSplit = true; if (arguments.length > 1) { options = [].slice.call(arguments); doSplit = false; } if (!Array.isArray(options)) { options = [options]; } this._currentInput.options(options.reduce(function(options, option) { var split = String(option).split(' '); if (doSplit && split.length === 2) { options.push(split[0], split[1]); } else { options.push(option); } return options; }, [])); return this; }; /** * Add custom output option(s) * * @example * command.outputOptions('option1'); * * @example * command.outputOptions('option1', 'option2'); * * @example * command.outputOptions(['option1', 'option2']); * * @method FfmpegCommand#outputOptions * @category Custom options * @aliases addOutputOption,addOutputOptions,addOption,addOptions,withOutputOption,withOutputOptions,withOption,withOptions,outputOption * * @param {...String} options option string(s) or string array * @return FfmpegCommand */ proto.addOutputOption = proto.addOutputOptions = proto.addOption = proto.addOptions = proto.withOutputOption = proto.withOutputOptions = proto.withOption = proto.withOptions = proto.outputOption = proto.outputOptions = function(options) { var doSplit = true; if (arguments.length > 1) { options = [].slice.call(arguments); doSplit = false; } if (!Array.isArray(options)) { options = [options]; } this._currentOutput.options(options.reduce(function(options, option) { var split = String(option).split(' '); if (doSplit && split.length === 2) { options.push(split[0], split[1]); } else { options.push(option); } return options; }, [])); return this; }; /** * Specify a complex filtergraph * * Calling this method will override any previously set filtergraph, but you can set * as many filters as needed in one call. * * @example Overlay an image over a video (using a filtergraph string) * ffmpeg() * .input('video.avi') * .input('image.png') * .complexFilter('[0:v][1:v]overlay[out]', ['out']); * * @example Overlay an image over a video (using a filter array) * ffmpeg() * .input('video.avi') * .input('image.png') * .complexFilter([{ * filter: 'overlay', * inputs: ['0:v', '1:v'], * outputs: ['out'] * }], ['out']); * * @example Split video into RGB channels and output a 3x1 video with channels side to side * ffmpeg() * .input('video.avi') * .complexFilter([ * // Duplicate video stream 3 times into streams a, b, and c * { filter: 'split', options: '3', outputs: ['a', 'b', 'c'] }, * * // Create stream 'red' by cancelling green and blue channels from stream 'a' * { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' }, * * // Create stream 'green' by cancelling red and blue channels from stream 'b' * { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' }, * * // Create stream 'blue' by cancelling red and green channels from stream 'c' * { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' }, * * // Pad stream 'red' to 3x width, keeping the video on the left, and name output 'padded' * { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' }, * * // Overlay 'green' onto 'padded', moving it to the center, and name output 'redgreen' * { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen'}, * * // Overlay 'blue' onto 'redgreen', moving it to the right * { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue']}, * ]); * * @method FfmpegCommand#complexFilter * @category Custom options * @aliases filterGraph * * @param {String|Array} spec filtergraph string or array of filter specification * objects, each having the following properties: * @param {String} spec.filter filter name * @param {String|Array} [spec.inputs] (array of) input stream specifier(s) for the filter, * defaults to ffmpeg automatically choosing the first unused matching streams * @param {String|Array} [spec.outputs] (array of) output stream specifier(s) for the filter, * defaults to ffmpeg automatically assigning the output to the output file * @param {Object|String|Array} [spec.options] filter options, can be omitted to not set any options * @param {Array} [map] (array of) stream specifier(s) from the graph to include in * ffmpeg output, defaults to ffmpeg automatically choosing the first matching streams. * @return FfmpegCommand */ proto.filterGraph = proto.complexFilter = function(spec, map) { this._complexFilters.clear(); if (!Array.isArray(spec)) { spec = [spec]; } this._complexFilters('-filter_complex', utils.makeFilterStrings(spec).join(';')); if (Array.isArray(map)) { var self = this; map.forEach(function(streamSpec) { self._complexFilters('-map', streamSpec.replace(utils.streamRegexp, '[$1]')); }); } else if (typeof map === 'string') { this._complexFilters('-map', map.replace(utils.streamRegexp, '[$1]')); } return this; }; }; ================================================ FILE: lib/options/inputs.js ================================================ /*jshint node:true*/ 'use strict'; var utils = require('../utils'); /* *! Input-related methods */ module.exports = function(proto) { /** * Add an input to command * * Also switches "current input", that is the input that will be affected * by subsequent input-related methods. * * Note: only one stream input is supported for now. * * @method FfmpegCommand#input * @category Input * @aliases mergeAdd,addInput * * @param {String|Readable} source input file path or readable stream * @return FfmpegCommand */ proto.mergeAdd = proto.addInput = proto.input = function(source) { var isFile = false; var isStream = false; if (typeof source !== 'string') { if (!('readable' in source) || !(source.readable)) { throw new Error('Invalid input'); } var hasInputStream = this._inputs.some(function(input) { return input.isStream; }); if (hasInputStream) { throw new Error('Only one input stream is supported'); } isStream = true; source.pause(); } else { var protocol = source.match(/^([a-z]{2,}):/i); isFile = !protocol || protocol[0] === 'file'; } this._inputs.push(this._currentInput = { source: source, isFile: isFile, isStream: isStream, options: utils.args() }); return this; }; /** * Specify input format for the last specified input * * @method FfmpegCommand#inputFormat * @category Input * @aliases withInputFormat,fromFormat * * @param {String} format input format * @return FfmpegCommand */ proto.withInputFormat = proto.inputFormat = proto.fromFormat = function(format) { if (!this._currentInput) { throw new Error('No input specified'); } this._currentInput.options('-f', format); return this; }; /** * Specify input FPS for the last specified input * (only valid for raw video formats) * * @method FfmpegCommand#inputFps * @category Input * @aliases withInputFps,withInputFPS,withFpsInput,withFPSInput,inputFPS,inputFps,fpsInput * * @param {Number} fps input FPS * @return FfmpegCommand */ proto.withInputFps = proto.withInputFPS = proto.withFpsInput = proto.withFPSInput = proto.inputFPS = proto.inputFps = proto.fpsInput = proto.FPSInput = function(fps) { if (!this._currentInput) { throw new Error('No input specified'); } this._currentInput.options('-r', fps); return this; }; /** * Use native framerate for the last specified input * * @method FfmpegCommand#native * @category Input * @aliases nativeFramerate,withNativeFramerate * * @return FfmmegCommand */ proto.nativeFramerate = proto.withNativeFramerate = proto.native = function() { if (!this._currentInput) { throw new Error('No input specified'); } this._currentInput.options('-re'); return this; }; /** * Specify input seek time for the last specified input * * @method FfmpegCommand#seekInput * @category Input * @aliases setStartTime,seekTo * * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string * @return FfmpegCommand */ proto.setStartTime = proto.seekInput = function(seek) { if (!this._currentInput) { throw new Error('No input specified'); } this._currentInput.options('-ss', seek); return this; }; /** * Loop over the last specified input * * @method FfmpegCommand#loop * @category Input * * @param {String|Number} [duration] loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string * @return FfmpegCommand */ proto.loop = function(duration) { if (!this._currentInput) { throw new Error('No input specified'); } this._currentInput.options('-loop', '1'); if (typeof duration !== 'undefined') { this.duration(duration); } return this; }; }; ================================================ FILE: lib/options/misc.js ================================================ /*jshint node:true*/ 'use strict'; var path = require('path'); /* *! Miscellaneous methods */ module.exports = function(proto) { /** * Use preset * * @method FfmpegCommand#preset * @category Miscellaneous * @aliases usingPreset * * @param {String|Function} preset preset name or preset function */ proto.usingPreset = proto.preset = function(preset) { if (typeof preset === 'function') { preset(this); } else { try { var modulePath = path.join(this.options.presets, preset); var module = require(modulePath); if (typeof module.load === 'function') { module.load(this); } else { throw new Error('preset ' + modulePath + ' has no load() function'); } } catch (err) { throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message); } } return this; }; }; ================================================ FILE: lib/options/output.js ================================================ /*jshint node:true*/ 'use strict'; var utils = require('../utils'); /* *! Output-related methods */ module.exports = function(proto) { /** * Add output * * @method FfmpegCommand#output * @category Output * @aliases addOutput * * @param {String|Writable} target target file path or writable stream * @param {Object} [pipeopts={}] pipe options (only applies to streams) * @return FfmpegCommand */ proto.addOutput = proto.output = function(target, pipeopts) { var isFile = false; if (!target && this._currentOutput) { // No target is only allowed when called from constructor throw new Error('Invalid output'); } if (target && typeof target !== 'string') { if (!('writable' in target) || !(target.writable)) { throw new Error('Invalid output'); } } else if (typeof target === 'string') { var protocol = target.match(/^([a-z]{2,}):/i); isFile = !protocol || protocol[0] === 'file'; } if (target && !('target' in this._currentOutput)) { // For backwards compatibility, set target for first output this._currentOutput.target = target; this._currentOutput.isFile = isFile; this._currentOutput.pipeopts = pipeopts || {}; } else { if (target && typeof target !== 'string') { var hasOutputStream = this._outputs.some(function(output) { return typeof output.target !== 'string'; }); if (hasOutputStream) { throw new Error('Only one output stream is supported'); } } this._outputs.push(this._currentOutput = { target: target, isFile: isFile, flags: {}, pipeopts: pipeopts || {} }); var self = this; ['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) { self._currentOutput[key] = utils.args(); }); if (!target) { // Call from constructor: remove target key delete this._currentOutput.target; } } return this; }; /** * Specify output seek time * * @method FfmpegCommand#seek * @category Input * @aliases seekOutput * * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string * @return FfmpegCommand */ proto.seekOutput = proto.seek = function(seek) { this._currentOutput.options('-ss', seek); return this; }; /** * Set output duration * * @method FfmpegCommand#duration * @category Output * @aliases withDuration,setDuration * * @param {String|Number} duration duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string * @return FfmpegCommand */ proto.withDuration = proto.setDuration = proto.duration = function(duration) { this._currentOutput.options('-t', duration); return this; }; /** * Set output format * * @method FfmpegCommand#format * @category Output * @aliases toFormat,withOutputFormat,outputFormat * * @param {String} format output format name * @return FfmpegCommand */ proto.toFormat = proto.withOutputFormat = proto.outputFormat = proto.format = function(format) { this._currentOutput.options('-f', format); return this; }; /** * Add stream mapping to output * * @method FfmpegCommand#map * @category Output * * @param {String} spec stream specification string, with optional square brackets * @return FfmpegCommand */ proto.map = function(spec) { this._currentOutput.options('-map', spec.replace(utils.streamRegexp, '[$1]')); return this; }; /** * Run flvtool2/flvmeta on output * * @method FfmpegCommand#flvmeta * @category Output * @aliases updateFlvMetadata * * @return FfmpegCommand */ proto.updateFlvMetadata = proto.flvmeta = function() { this._currentOutput.flags.flvmeta = true; return this; }; }; ================================================ FILE: lib/options/video.js ================================================ /*jshint node:true*/ 'use strict'; var utils = require('../utils'); /* *! Video-related methods */ module.exports = function(proto) { /** * Disable video in the output * * @method FfmpegCommand#noVideo * @category Video * @aliases withNoVideo * * @return FfmpegCommand */ proto.withNoVideo = proto.noVideo = function() { this._currentOutput.video.clear(); this._currentOutput.videoFilters.clear(); this._currentOutput.video('-vn'); return this; }; /** * Specify video codec * * @method FfmpegCommand#videoCodec * @category Video * @aliases withVideoCodec * * @param {String} codec video codec name * @return FfmpegCommand */ proto.withVideoCodec = proto.videoCodec = function(codec) { this._currentOutput.video('-vcodec', codec); return this; }; /** * Specify video bitrate * * @method FfmpegCommand#videoBitrate * @category Video * @aliases withVideoBitrate * * @param {String|Number} bitrate video bitrate in kbps (with an optional 'k' suffix) * @param {Boolean} [constant=false] enforce constant bitrate * @return FfmpegCommand */ proto.withVideoBitrate = proto.videoBitrate = function(bitrate, constant) { bitrate = ('' + bitrate).replace(/k?$/, 'k'); this._currentOutput.video('-b:v', bitrate); if (constant) { this._currentOutput.video( '-maxrate', bitrate, '-minrate', bitrate, '-bufsize', '3M' ); } return this; }; /** * Specify custom video filter(s) * * Can be called both with one or many filters, or a filter array. * * @example * command.videoFilters('filter1'); * * @example * command.videoFilters('filter1', 'filter2=param1=value1:param2=value2'); * * @example * command.videoFilters(['filter1', 'filter2']); * * @example * command.videoFilters([ * { * filter: 'filter1' * }, * { * filter: 'filter2', * options: 'param=value:param=value' * } * ]); * * @example * command.videoFilters( * { * filter: 'filter1', * options: ['value1', 'value2'] * }, * { * filter: 'filter2', * options: { param1: 'value1', param2: 'value2' } * } * ); * * @method FfmpegCommand#videoFilters * @category Video * @aliases withVideoFilter,withVideoFilters,videoFilter * * @param {...String|String[]|Object[]} filters video filter strings, string array or * filter specification array, each with the following properties: * @param {String} filters.filter filter name * @param {String|String[]|Object} [filters.options] filter option string, array, or object * @return FfmpegCommand */ proto.withVideoFilter = proto.withVideoFilters = proto.videoFilter = proto.videoFilters = function(filters) { if (arguments.length > 1) { filters = [].slice.call(arguments); } if (!Array.isArray(filters)) { filters = [filters]; } this._currentOutput.videoFilters(utils.makeFilterStrings(filters)); return this; }; /** * Specify output FPS * * @method FfmpegCommand#fps * @category Video * @aliases withOutputFps,withOutputFPS,withFpsOutput,withFPSOutput,withFps,withFPS,outputFPS,outputFps,fpsOutput,FPSOutput,FPS * * @param {Number} fps output FPS * @return FfmpegCommand */ proto.withOutputFps = proto.withOutputFPS = proto.withFpsOutput = proto.withFPSOutput = proto.withFps = proto.withFPS = proto.outputFPS = proto.outputFps = proto.fpsOutput = proto.FPSOutput = proto.fps = proto.FPS = function(fps) { this._currentOutput.video('-r', fps); return this; }; /** * Only transcode a certain number of frames * * @method FfmpegCommand#frames * @category Video * @aliases takeFrames,withFrames * * @param {Number} frames frame count * @return FfmpegCommand */ proto.takeFrames = proto.withFrames = proto.frames = function(frames) { this._currentOutput.video('-vframes', frames); return this; }; }; ================================================ FILE: lib/options/videosize.js ================================================ /*jshint node:true*/ 'use strict'; /* *! Size helpers */ /** * Return filters to pad video to width*height, * * @param {Number} width output width * @param {Number} height output height * @param {Number} aspect video aspect ratio (without padding) * @param {Number} color padding color * @return scale/pad filters * @private */ function getScalePadFilters(width, height, aspect, color) { /* let a be the input aspect ratio, A be the requested aspect ratio if a > A, padding is done on top and bottom if a < A, padding is done on left and right */ return [ /* In both cases, we first have to scale the input to match the requested size. When using computed width/height, we truncate them to multiples of 2 */ { filter: 'scale', options: { w: 'if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2)', h: 'if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)' } }, /* Then we pad the scaled input to match the target size (here iw and ih refer to the padding input, i.e the scaled output) */ { filter: 'pad', options: { w: width, h: height, x: 'if(gt(a,' + aspect + '),0,(' + width + '-iw)/2)', y: 'if(lt(a,' + aspect + '),0,(' + height + '-ih)/2)', color: color } } ]; } /** * Recompute size filters * * @param {Object} output * @param {String} key newly-added parameter name ('size', 'aspect' or 'pad') * @param {String} value newly-added parameter value * @return filter string array * @private */ function createSizeFilters(output, key, value) { // Store parameters var data = output.sizeData = output.sizeData || {}; data[key] = value; if (!('size' in data)) { // No size requested, keep original size return []; } // Try to match the different size string formats var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/); var fixedWidth = data.size.match(/([0-9]+)x\?/); var fixedHeight = data.size.match(/\?x([0-9]+)/); var percentRatio = data.size.match(/\b([0-9]{1,3})%/); var width, height, aspect; if (percentRatio) { var ratio = Number(percentRatio[1]) / 100; return [{ filter: 'scale', options: { w: 'trunc(iw*' + ratio + '/2)*2', h: 'trunc(ih*' + ratio + '/2)*2' } }]; } else if (fixedSize) { // Round target size to multiples of 2 width = Math.round(Number(fixedSize[1]) / 2) * 2; height = Math.round(Number(fixedSize[2]) / 2) * 2; aspect = width / height; if (data.pad) { return getScalePadFilters(width, height, aspect, data.pad); } else { // No autopad requested, rescale to target size return [{ filter: 'scale', options: { w: width, h: height }}]; } } else if (fixedWidth || fixedHeight) { if ('aspect' in data) { // Specified aspect ratio width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect); height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect); // Round to multiples of 2 width = Math.round(width / 2) * 2; height = Math.round(height / 2) * 2; if (data.pad) { return getScalePadFilters(width, height, data.aspect, data.pad); } else { // No autopad requested, rescale to target size return [{ filter: 'scale', options: { w: width, h: height }}]; } } else { // Keep input aspect ratio if (fixedWidth) { return [{ filter: 'scale', options: { w: Math.round(Number(fixedWidth[1]) / 2) * 2, h: 'trunc(ow/a/2)*2' } }]; } else { return [{ filter: 'scale', options: { w: 'trunc(oh*a/2)*2', h: Math.round(Number(fixedHeight[1]) / 2) * 2 } }]; } } } else { throw new Error('Invalid size specified: ' + data.size); } } /* *! Video size-related methods */ module.exports = function(proto) { /** * Keep display aspect ratio * * This method is useful when converting an input with non-square pixels to an output format * that does not support non-square pixels. It rescales the input so that the display aspect * ratio is the same. * * @method FfmpegCommand#keepDAR * @category Video size * @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio * * @return FfmpegCommand */ proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio proto.keepDisplayAspect = proto.keepDisplayAspectRatio = proto.keepDAR = function() { return this.videoFilters([ { filter: 'scale', options: { w: 'if(gt(sar,1),iw*sar,iw)', h: 'if(lt(sar,1),ih/sar,ih)' } }, { filter: 'setsar', options: '1' } ]); }; /** * Set output size * * The 'size' parameter can have one of 4 forms: * - 'X%': rescale to xx % of the original size * - 'WxH': specify width and height * - 'Wx?': specify width and compute height from input aspect ratio * - '?xH': specify height and compute width from input aspect ratio * * Note: both dimensions will be truncated to multiples of 2. * * @method FfmpegCommand#size * @category Video size * @aliases withSize,setSize * * @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240' * @return FfmpegCommand */ proto.withSize = proto.setSize = proto.size = function(size) { var filters = createSizeFilters(this._currentOutput, 'size', size); this._currentOutput.sizeFilters.clear(); this._currentOutput.sizeFilters(filters); return this; }; /** * Set output aspect ratio * * @method FfmpegCommand#aspect * @category Video size * @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio * * @param {String|Number} aspect aspect ratio (number or 'X:Y' string) * @return FfmpegCommand */ proto.withAspect = proto.withAspectRatio = proto.setAspect = proto.setAspectRatio = proto.aspect = proto.aspectRatio = function(aspect) { var a = Number(aspect); if (isNaN(a)) { var match = aspect.match(/^(\d+):(\d+)$/); if (match) { a = Number(match[1]) / Number(match[2]); } else { throw new Error('Invalid aspect ratio: ' + aspect); } } var filters = createSizeFilters(this._currentOutput, 'aspect', a); this._currentOutput.sizeFilters.clear(); this._currentOutput.sizeFilters(filters); return this; }; /** * Enable auto-padding the output * * @method FfmpegCommand#autopad * @category Video size * @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad * * @param {Boolean} [pad=true] enable/disable auto-padding * @param {String} [color='black'] pad color */ proto.applyAutopadding = proto.applyAutoPadding = proto.applyAutopad = proto.applyAutoPad = proto.withAutopadding = proto.withAutoPadding = proto.withAutopad = proto.withAutoPad = proto.autoPad = proto.autopad = function(pad, color) { // Allow autopad(color) if (typeof pad === 'string') { color = pad; pad = true; } // Allow autopad() and autopad(undefined, color) if (typeof pad === 'undefined') { pad = true; } var filters = createSizeFilters(this._currentOutput, 'pad', pad ? color || 'black' : false); this._currentOutput.sizeFilters.clear(); this._currentOutput.sizeFilters(filters); return this; }; }; ================================================ FILE: lib/presets/divx.js ================================================ /*jshint node:true */ 'use strict'; exports.load = function(ffmpeg) { ffmpeg .format('avi') .videoBitrate('1024k') .videoCodec('mpeg4') .size('720x?') .audioBitrate('128k') .audioChannels(2) .audioCodec('libmp3lame') .outputOptions(['-vtag DIVX']); }; ================================================ FILE: lib/presets/flashvideo.js ================================================ /*jshint node:true */ 'use strict'; exports.load = function(ffmpeg) { ffmpeg .format('flv') .flvmeta() .size('320x?') .videoBitrate('512k') .videoCodec('libx264') .fps(24) .audioBitrate('96k') .audioCodec('aac') .audioFrequency(22050) .audioChannels(2); }; ================================================ FILE: lib/presets/podcast.js ================================================ /*jshint node:true */ 'use strict'; exports.load = function(ffmpeg) { ffmpeg .format('m4v') .videoBitrate('512k') .videoCodec('libx264') .size('320x176') .audioBitrate('128k') .audioCodec('aac') .audioChannels(1) .outputOptions(['-flags', '+loop', '-cmp', '+chroma', '-partitions','+parti4x4+partp8x8+partb8x8', '-flags2', '+mixed_refs', '-me_method umh', '-subq 5', '-bufsize 2M', '-rc_eq \'blurCplx^(1-qComp)\'', '-qcomp 0.6', '-qmin 10', '-qmax 51', '-qdiff 4', '-level 13' ]); }; ================================================ FILE: lib/processor.js ================================================ /*jshint node:true*/ 'use strict'; var spawn = require('child_process').spawn; var path = require('path'); var fs = require('fs'); var async = require('async'); var utils = require('./utils'); /* *! Processor methods */ /** * Run ffprobe asynchronously and store data in command * * @param {FfmpegCommand} command * @private */ function runFfprobe(command) { const inputProbeIndex = 0; if (command._inputs[inputProbeIndex].isStream) { // Don't probe input streams as this will consume them return; } command.ffprobe(inputProbeIndex, function(err, data) { command._ffprobeData = data; }); } module.exports = function(proto) { /** * Emitted just after ffmpeg has been spawned. * * @event FfmpegCommand#start * @param {String} command ffmpeg command line */ /** * Emitted when ffmpeg reports progress information * * @event FfmpegCommand#progress * @param {Object} progress progress object * @param {Number} progress.frames number of frames transcoded * @param {Number} progress.currentFps current processing speed in frames per second * @param {Number} progress.currentKbps current output generation speed in kilobytes per second * @param {Number} progress.targetSize current output file size * @param {String} progress.timemark current video timemark * @param {Number} [progress.percent] processing progress (may not be available depending on input) */ /** * Emitted when ffmpeg outputs to stderr * * @event FfmpegCommand#stderr * @param {String} line stderr output line */ /** * Emitted when ffmpeg reports input codec data * * @event FfmpegCommand#codecData * @param {Object} codecData codec data object * @param {String} codecData.format input format name * @param {String} codecData.audio input audio codec name * @param {String} codecData.audio_details input audio codec parameters * @param {String} codecData.video input video codec name * @param {String} codecData.video_details input video codec parameters */ /** * Emitted when an error happens when preparing or running a command * * @event FfmpegCommand#error * @param {Error} error error object, with optional properties 'inputStreamError' / 'outputStreamError' for errors on their respective streams * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream * @param {String|null} stderr ffmpeg stderr */ /** * Emitted when a command finishes processing * * @event FfmpegCommand#end * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise * @param {String|null} stderr ffmpeg stderr */ /** * Spawn an ffmpeg process * * The 'options' argument may contain the following keys: * - 'niceness': specify process niceness, ignored on Windows (default: 0) * - `cwd`: change working directory * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false) * - 'stdoutLines': override command limit (default: use command limit) * * The 'processCB' callback, if present, is called as soon as the process is created and * receives a nodejs ChildProcess object. It may not be called at all if an error happens * before spawning the process. * * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes. * * @method FfmpegCommand#_spawnFfmpeg * @param {Array} args ffmpeg command line argument list * @param {Object} [options] spawn options (see above) * @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created * @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished * @private */ proto._spawnFfmpeg = function(args, options, processCB, endCB) { // Enable omitting options if (typeof options === 'function') { endCB = processCB; processCB = options; options = {}; } // Enable omitting processCB if (typeof endCB === 'undefined') { endCB = processCB; processCB = function() {}; } var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines; // Find ffmpeg this._getFfmpegPath(function(err, command) { if (err) { return endCB(err); } else if (!command || command.length === 0) { return endCB(new Error('Cannot find ffmpeg')); } // Apply niceness if (options.niceness && options.niceness !== 0 && !utils.isWindows) { args.unshift('-n', options.niceness, command); command = 'nice'; } var stdoutRing = utils.linesRing(maxLines); var stdoutClosed = false; var stderrRing = utils.linesRing(maxLines); var stderrClosed = false; // Spawn process var ffmpegProc = spawn(command, args, options); if (ffmpegProc.stderr) { ffmpegProc.stderr.setEncoding('utf8'); } ffmpegProc.on('error', function(err) { endCB(err); }); // Ensure we wait for captured streams to end before calling endCB var exitError = null; function handleExit(err) { if (err) { exitError = err; } if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) { endCB(exitError, stdoutRing, stderrRing); } } // Handle process exit var processExited = false; ffmpegProc.on('exit', function(code, signal) { processExited = true; if (signal) { handleExit(new Error('ffmpeg was killed with signal ' + signal)); } else if (code) { handleExit(new Error('ffmpeg exited with code ' + code)); } else { handleExit(); } }); // Capture stdout if specified if (options.captureStdout) { ffmpegProc.stdout.on('data', function(data) { stdoutRing.append(data); }); ffmpegProc.stdout.on('close', function() { stdoutRing.close(); stdoutClosed = true; handleExit(); }); } // Capture stderr if specified ffmpegProc.stderr.on('data', function(data) { stderrRing.append(data); }); ffmpegProc.stderr.on('close', function() { stderrRing.close(); stderrClosed = true; handleExit(); }); // Call process callback processCB(ffmpegProc, stdoutRing, stderrRing); }); }; /** * Build the argument list for an ffmpeg command * * @method FfmpegCommand#_getArguments * @return argument list * @private */ proto._getArguments = function() { var complexFilters = this._complexFilters.get(); var fileOutput = this._outputs.some(function(output) { return output.isFile; }); return [].concat( // Inputs and input options this._inputs.reduce(function(args, input) { var source = (typeof input.source === 'string') ? input.source : 'pipe:0'; // For each input, add input options, then '-i ' return args.concat( input.options.get(), ['-i', source] ); }, []), // Global options this._global.get(), // Overwrite if we have file outputs fileOutput ? ['-y'] : [], // Complex filters complexFilters, // Outputs, filters and output options this._outputs.reduce(function(args, output) { var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get()); var audioFilters = output.audioFilters.get(); var videoFilters = output.videoFilters.get().concat(sizeFilters); var outputArg; if (!output.target) { outputArg = []; } else if (typeof output.target === 'string') { outputArg = [output.target]; } else { outputArg = ['pipe:1']; } return args.concat( output.audio.get(), audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [], output.video.get(), videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [], output.options.get(), outputArg ); }, []) ); }; /** * Prepare execution of an ffmpeg command * * Checks prerequisites for the execution of the command (codec/format availability, flvtool...), * then builds the argument list for ffmpeg and pass them to 'callback'. * * @method FfmpegCommand#_prepare * @param {Function} callback callback with signature (err, args) * @param {Boolean} [readMetadata=false] read metadata before processing * @private */ proto._prepare = function(callback, readMetadata) { var self = this; async.waterfall([ // Check codecs and formats function(cb) { self._checkCapabilities(cb); }, // Read metadata if required function(cb) { if (!readMetadata) { return cb(); } self.ffprobe(0, function(err, data) { if (!err) { self._ffprobeData = data; } cb(); }); }, // Check for flvtool2/flvmeta if necessary function(cb) { var flvmeta = self._outputs.some(function(output) { // Remove flvmeta flag on non-file output if (output.flags.flvmeta && !output.isFile) { self.logger.warn('Updating flv metadata is only supported for files'); output.flags.flvmeta = false; } return output.flags.flvmeta; }); if (flvmeta) { self._getFlvtoolPath(function(err) { cb(err); }); } else { cb(); } }, // Build argument list function(cb) { var args; try { args = self._getArguments(); } catch(e) { return cb(e); } cb(null, args); }, // Add "-strict experimental" option where needed function(args, cb) { self.availableEncoders(function(err, encoders) { for (var i = 0; i < args.length; i++) { if (args[i] === '-acodec' || args[i] === '-vcodec') { i++; if ((args[i] in encoders) && encoders[args[i]].experimental) { args.splice(i + 1, 0, '-strict', 'experimental'); i += 2; } } } cb(null, args); }); } ], callback); if (!readMetadata) { // Read metadata as soon as 'progress' listeners are added if (this.listeners('progress').length > 0) { // Read metadata in parallel runFfprobe(this); } else { // Read metadata as soon as the first 'progress' listener is added this.once('newListener', function(event) { if (event === 'progress') { runFfprobe(this); } }); } } }; /** * Run ffmpeg command * * @method FfmpegCommand#run * @category Processing * @aliases exec,execute */ proto.exec = proto.execute = proto.run = function() { var self = this; // Check if at least one output is present var outputPresent = this._outputs.some(function(output) { return 'target' in output; }); if (!outputPresent) { throw new Error('No output specified'); } // Get output stream if any var outputStream = this._outputs.filter(function(output) { return typeof output.target !== 'string'; })[0]; // Get input stream if any var inputStream = this._inputs.filter(function(input) { return typeof input.source !== 'string'; })[0]; // Ensure we send 'end' or 'error' only once var ended = false; function emitEnd(err, stdout, stderr) { if (!ended) { ended = true; if (err) { self.emit('error', err, stdout, stderr); } else { self.emit('end', stdout, stderr); } } } self._prepare(function(err, args) { if (err) { return emitEnd(err); } // Run ffmpeg self._spawnFfmpeg( args, { captureStdout: !outputStream, niceness: self.options.niceness, cwd: self.options.cwd, windowsHide: true }, function processCB(ffmpegProc, stdoutRing, stderrRing) { self.ffmpegProc = ffmpegProc; self.emit('start', 'ffmpeg ' + args.join(' ')); // Pipe input stream if any if (inputStream) { inputStream.source.on('error', function(err) { var reportingErr = new Error('Input stream error: ' + err.message); reportingErr.inputStreamError = err; emitEnd(reportingErr); ffmpegProc.kill(); }); inputStream.source.resume(); inputStream.source.pipe(ffmpegProc.stdin); // Set stdin error handler on ffmpeg (prevents nodejs catching the error, but // ffmpeg will fail anyway, so no need to actually handle anything) ffmpegProc.stdin.on('error', function() {}); } // Setup timeout if requested if (self.options.timeout) { self.processTimer = setTimeout(function() { var msg = 'process ran into a timeout (' + self.options.timeout + 's)'; emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get()); ffmpegProc.kill(); }, self.options.timeout * 1000); } if (outputStream) { // Pipe ffmpeg stdout to output stream ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts); // Handle output stream events outputStream.target.on('close', function() { self.logger.debug('Output stream closed, scheduling kill for ffmpeg process'); // Don't kill process yet, to give a chance to ffmpeg to // terminate successfully first This is necessary because // under load, the process 'exit' event sometimes happens // after the output stream 'close' event. setTimeout(function() { emitEnd(new Error('Output stream closed')); ffmpegProc.kill(); }, 20); }); outputStream.target.on('error', function(err) { self.logger.debug('Output stream error, killing ffmpeg process'); var reportingErr = new Error('Output stream error: ' + err.message); reportingErr.outputStreamError = err; emitEnd(reportingErr, stdoutRing.get(), stderrRing.get()); ffmpegProc.kill('SIGKILL'); }); } // Setup stderr handling if (stderrRing) { // 'stderr' event if (self.listeners('stderr').length) { stderrRing.callback(function(line) { self.emit('stderr', line); }); } // 'codecData' event if (self.listeners('codecData').length) { var codecDataSent = false; var codecObject = {}; stderrRing.callback(function(line) { if (!codecDataSent) codecDataSent = utils.extractCodecData(self, line, codecObject); }); } // 'progress' event if (self.listeners('progress').length) { stderrRing.callback(function(line) { utils.extractProgress(self, line); }); } } }, function endCB(err, stdoutRing, stderrRing) { clearTimeout(self.processTimer); delete self.ffmpegProc; if (err) { if (err.message.match(/ffmpeg exited with code/)) { // Add ffmpeg error message err.message += ': ' + utils.extractError(stderrRing.get()); } emitEnd(err, stdoutRing.get(), stderrRing.get()); } else { // Find out which outputs need flv metadata var flvmeta = self._outputs.filter(function(output) { return output.flags.flvmeta; }); if (flvmeta.length) { self._getFlvtoolPath(function(err, flvtool) { if (err) { return emitEnd(err); } async.each( flvmeta, function(output, cb) { spawn(flvtool, ['-U', output.target], {windowsHide: true}) .on('error', function(err) { cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message)); }) .on('exit', function(code, signal) { if (code !== 0 || signal) { cb( new Error(flvtool + ' ' + (signal ? 'received signal ' + signal : 'exited with code ' + code)) + ' when running on ' + output.target ); } else { cb(); } }); }, function(err) { if (err) { emitEnd(err); } else { emitEnd(null, stdoutRing.get(), stderrRing.get()); } } ); }); } else { emitEnd(null, stdoutRing.get(), stderrRing.get()); } } } ); }); return this; }; /** * Renice current and/or future ffmpeg processes * * Ignored on Windows platforms. * * @method FfmpegCommand#renice * @category Processing * * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority) * @return FfmpegCommand */ proto.renice = function(niceness) { if (!utils.isWindows) { niceness = niceness || 0; if (niceness < -20 || niceness > 20) { this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20'); } niceness = Math.min(20, Math.max(-20, niceness)); this.options.niceness = niceness; if (this.ffmpegProc) { var logger = this.logger; var pid = this.ffmpegProc.pid; var renice = spawn('renice', [niceness, '-p', pid], {windowsHide: true}); renice.on('error', function(err) { logger.warn('could not renice process ' + pid + ': ' + err.message); }); renice.on('exit', function(code, signal) { if (signal) { logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal); } else if (code) { logger.warn('could not renice process ' + pid + ': renice exited with ' + code); } else { logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness'); } }); } } return this; }; /** * Kill current ffmpeg process, if any * * @method FfmpegCommand#kill * @category Processing * * @param {String} [signal=SIGKILL] signal name * @return FfmpegCommand */ proto.kill = function(signal) { if (!this.ffmpegProc) { this.logger.warn('No running ffmpeg process, cannot send signal'); } else { this.ffmpegProc.kill(signal || 'SIGKILL'); } return this; }; }; ================================================ FILE: lib/recipes.js ================================================ /*jshint node:true*/ 'use strict'; var fs = require('fs'); var path = require('path'); var PassThrough = require('stream').PassThrough; var async = require('async'); var utils = require('./utils'); /* * Useful recipes for commands */ module.exports = function recipes(proto) { /** * Execute ffmpeg command and save output to a file * * @method FfmpegCommand#save * @category Processing * @aliases saveToFile * * @param {String} output file path * @return FfmpegCommand */ proto.saveToFile = proto.save = function(output) { this.output(output).run(); return this; }; /** * Execute ffmpeg command and save output to a stream * * If 'stream' is not specified, a PassThrough stream is created and returned. * 'options' will be used when piping ffmpeg output to the output stream * (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options) * * @method FfmpegCommand#pipe * @category Processing * @aliases stream,writeToStream * * @param {stream.Writable} [stream] output stream * @param {Object} [options={}] pipe options * @return Output stream */ proto.writeToStream = proto.pipe = proto.stream = function(stream, options) { if (stream && !('writable' in stream)) { options = stream; stream = undefined; } if (!stream) { if (process.version.match(/v0\.8\./)) { throw new Error('PassThrough stream is not supported on node v0.8'); } stream = new PassThrough(); } this.output(stream, options).run(); return stream; }; /** * Generate images from a video * * Note: this method makes the command emit a 'filenames' event with an array of * the generated image filenames. * * @method FfmpegCommand#screenshots * @category Processing * @aliases takeScreenshots,thumbnail,thumbnails,screenshot * * @param {Number|Object} [config=1] screenshot count or configuration object with * the following keys: * @param {Number} [config.count] number of screenshots to take; using this option * takes screenshots at regular intervals (eg. count=4 would take screens at 20%, 40%, * 60% and 80% of the video length). * @param {String} [config.folder='.'] output folder * @param {String} [config.filename='tn.png'] output filename pattern, may contain the following * tokens: * - '%s': offset in seconds * - '%w': screenshot width * - '%h': screenshot height * - '%r': screenshot resolution (same as '%wx%h') * - '%f': input filename * - '%b': input basename (filename w/o extension) * - '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`) * @param {Number[]|String[]} [config.timemarks] array of timemarks to take screenshots * at; each timemark may be a number of seconds, a '[[hh:]mm:]ss[.xxx]' string or a * 'XX%' string. Overrides 'count' if present. * @param {Number[]|String[]} [config.timestamps] alias for 'timemarks' * @param {Boolean} [config.fastSeek] use fast seek (less accurate) * @param {String} [config.size] screenshot size, with the same syntax as {@link FfmpegCommand#size} * @param {String} [folder] output folder (legacy alias for 'config.folder') * @return FfmpegCommand */ proto.takeScreenshots = proto.thumbnail = proto.thumbnails = proto.screenshot = proto.screenshots = function(config, folder) { var self = this; var source = this._currentInput.source; config = config || { count: 1 }; // Accept a number of screenshots instead of a config object if (typeof config === 'number') { config = { count: config }; } // Accept a second 'folder' parameter instead of config.folder if (!('folder' in config)) { config.folder = folder || '.'; } // Accept 'timestamps' instead of 'timemarks' if ('timestamps' in config) { config.timemarks = config.timestamps; } // Compute timemarks from count if not present if (!('timemarks' in config)) { if (!config.count) { throw new Error('Cannot take screenshots: neither a count nor a timemark list are specified'); } var interval = 100 / (1 + config.count); config.timemarks = []; for (var i = 0; i < config.count; i++) { config.timemarks.push((interval * (i + 1)) + '%'); } } // Parse size option if ('size' in config) { var fixedSize = config.size.match(/^(\d+)x(\d+)$/); var fixedWidth = config.size.match(/^(\d+)x\?$/); var fixedHeight = config.size.match(/^\?x(\d+)$/); var percentSize = config.size.match(/^(\d+)%$/); if (!fixedSize && !fixedWidth && !fixedHeight && !percentSize) { throw new Error('Invalid size parameter: ' + config.size); } } // Metadata helper var metadata; function getMetadata(cb) { if (metadata) { cb(null, metadata); } else { self.ffprobe(function(err, meta) { metadata = meta; cb(err, meta); }); } } async.waterfall([ // Compute percent timemarks if any function computeTimemarks(next) { if (config.timemarks.some(function(t) { return ('' + t).match(/^[\d.]+%$/); })) { if (typeof source !== 'string') { return next(new Error('Cannot compute screenshot timemarks with an input stream, please specify fixed timemarks')); } getMetadata(function(err, meta) { if (err) { next(err); } else { // Select video stream with the highest resolution var vstream = meta.streams.reduce(function(biggest, stream) { if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) { return stream; } else { return biggest; } }, { width: 0, height: 0 }); if (vstream.width === 0) { return next(new Error('No video stream in input, cannot take screenshots')); } var duration = Number(vstream.duration); if (isNaN(duration)) { duration = Number(meta.format.duration); } if (isNaN(duration)) { return next(new Error('Could not get input duration, please specify fixed timemarks')); } config.timemarks = config.timemarks.map(function(mark) { if (('' + mark).match(/^([\d.]+)%$/)) { return duration * parseFloat(mark) / 100; } else { return mark; } }); next(); } }); } else { next(); } }, // Turn all timemarks into numbers and sort them function normalizeTimemarks(next) { config.timemarks = config.timemarks.map(function(mark) { return utils.timemarkToSeconds(mark); }).sort(function(a, b) { return a - b; }); next(); }, // Add '_%i' to pattern when requesting multiple screenshots and no variable token is present function fixPattern(next) { var pattern = config.filename || 'tn.png'; if (pattern.indexOf('.') === -1) { pattern += '.png'; } if (config.timemarks.length > 1 && !pattern.match(/%(s|0*i)/)) { var ext = path.extname(pattern); pattern = path.join(path.dirname(pattern), path.basename(pattern, ext) + '_%i' + ext); } next(null, pattern); }, // Replace filename tokens (%f, %b) in pattern function replaceFilenameTokens(pattern, next) { if (pattern.match(/%[bf]/)) { if (typeof source !== 'string') { return next(new Error('Cannot replace %f or %b when using an input stream')); } pattern = pattern .replace(/%f/g, path.basename(source)) .replace(/%b/g, path.basename(source, path.extname(source))); } next(null, pattern); }, // Compute size if needed function getSize(pattern, next) { if (pattern.match(/%[whr]/)) { if (fixedSize) { return next(null, pattern, fixedSize[1], fixedSize[2]); } getMetadata(function(err, meta) { if (err) { return next(new Error('Could not determine video resolution to replace %w, %h or %r')); } var vstream = meta.streams.reduce(function(biggest, stream) { if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) { return stream; } else { return biggest; } }, { width: 0, height: 0 }); if (vstream.width === 0) { return next(new Error('No video stream in input, cannot replace %w, %h or %r')); } var width = vstream.width; var height = vstream.height; if (fixedWidth) { height = height * Number(fixedWidth[1]) / width; width = Number(fixedWidth[1]); } else if (fixedHeight) { width = width * Number(fixedHeight[1]) / height; height = Number(fixedHeight[1]); } else if (percentSize) { width = width * Number(percentSize[1]) / 100; height = height * Number(percentSize[1]) / 100; } next(null, pattern, Math.round(width / 2) * 2, Math.round(height / 2) * 2); }); } else { next(null, pattern, -1, -1); } }, // Replace size tokens (%w, %h, %r) in pattern function replaceSizeTokens(pattern, width, height, next) { pattern = pattern .replace(/%r/g, '%wx%h') .replace(/%w/g, width) .replace(/%h/g, height); next(null, pattern); }, // Replace variable tokens in pattern (%s, %i) and generate filename list function replaceVariableTokens(pattern, next) { var filenames = config.timemarks.map(function(t, i) { return pattern .replace(/%s/g, utils.timemarkToSeconds(t)) .replace(/%(0*)i/g, function(match, padding) { var idx = '' + (i + 1); return padding.substr(0, Math.max(0, padding.length + 1 - idx.length)) + idx; }); }); self.emit('filenames', filenames); next(null, filenames); }, // Create output directory function createDirectory(filenames, next) { fs.exists(config.folder, function(exists) { if (!exists) { fs.mkdir(config.folder, function(err) { if (err) { next(err); } else { next(null, filenames); } }); } else { next(null, filenames); } }); } ], function runCommand(err, filenames) { if (err) { return self.emit('error', err); } var count = config.timemarks.length; var split; var filters = [split = { filter: 'split', options: count, outputs: [] }]; if ('size' in config) { // Set size to generate size filters self.size(config.size); // Get size filters and chain them with 'sizeN' stream names var sizeFilters = self._currentOutput.sizeFilters.get().map(function(f, i) { if (i > 0) { f.inputs = 'size' + (i - 1); } f.outputs = 'size' + i; return f; }); // Input last size filter output into split filter split.inputs = 'size' + (sizeFilters.length - 1); // Add size filters in front of split filter filters = sizeFilters.concat(filters); // Remove size filters self._currentOutput.sizeFilters.clear(); } var first = 0; for (var i = 0; i < count; i++) { var stream = 'screen' + i; split.outputs.push(stream); if (i === 0) { first = config.timemarks[i]; self.seekInput(first); } self.output(path.join(config.folder, filenames[i])) .frames(1) .map(stream); if (i > 0) { self.seek(config.timemarks[i] - first); } } self.complexFilter(filters); self.run(); }); return this; }; /** * Merge (concatenate) inputs to a single file * * @method FfmpegCommand#concat * @category Processing * @aliases concatenate,mergeToFile * * @param {String|Writable} target output file or writable stream * @param {Object} [options] pipe options (only used when outputting to a writable stream) * @return FfmpegCommand */ proto.mergeToFile = proto.concatenate = proto.concat = function(target, options) { // Find out which streams are present in the first non-stream input var fileInput = this._inputs.filter(function(input) { return !input.isStream; })[0]; var self = this; this.ffprobe(this._inputs.indexOf(fileInput), function(err, data) { if (err) { return self.emit('error', err); } var hasAudioStreams = data.streams.some(function(stream) { return stream.codec_type === 'audio'; }); var hasVideoStreams = data.streams.some(function(stream) { return stream.codec_type === 'video'; }); // Setup concat filter and start processing self.output(target, options) .complexFilter({ filter: 'concat', options: { n: self._inputs.length, v: hasVideoStreams ? 1 : 0, a: hasAudioStreams ? 1 : 0 } }) .run(); }); return this; }; }; ================================================ FILE: lib/utils.js ================================================ /*jshint node:true*/ 'use strict'; var exec = require('child_process').exec; var isWindows = require('os').platform().match(/win(32|64)/); var which = require('which'); var nlRegexp = /\r\n|\r|\n/g; var streamRegexp = /^\[?(.*?)\]?$/; var filterEscapeRegexp = /[,]/; var whichCache = {}; /** * Parse progress line from ffmpeg stderr * * @param {String} line progress line * @return progress object * @private */ function parseProgressLine(line) { var progress = {}; // Remove all spaces after = and trim line = line.replace(/=\s+/g, '=').trim(); var progressParts = line.split(' '); // Split every progress part by "=" to get key and value for(var i = 0; i < progressParts.length; i++) { var progressSplit = progressParts[i].split('=', 2); var key = progressSplit[0]; var value = progressSplit[1]; // This is not a progress line if(typeof value === 'undefined') return null; progress[key] = value; } return progress; } var utils = module.exports = { isWindows: isWindows, streamRegexp: streamRegexp, /** * Copy an object keys into another one * * @param {Object} source source object * @param {Object} dest destination object * @private */ copy: function(source, dest) { Object.keys(source).forEach(function(key) { dest[key] = source[key]; }); }, /** * Create an argument list * * Returns a function that adds new arguments to the list. * It also has the following methods: * - clear() empties the argument list * - get() returns the argument list * - find(arg, count) finds 'arg' in the list and return the following 'count' items, or undefined if not found * - remove(arg, count) remove 'arg' in the list as well as the following 'count' items * * @private */ args: function() { var list = []; // Append argument(s) to the list var argfunc = function() { if (arguments.length === 1 && Array.isArray(arguments[0])) { list = list.concat(arguments[0]); } else { list = list.concat([].slice.call(arguments)); } }; // Clear argument list argfunc.clear = function() { list = []; }; // Return argument list argfunc.get = function() { return list; }; // Find argument 'arg' in list, and if found, return an array of the 'count' items that follow it argfunc.find = function(arg, count) { var index = list.indexOf(arg); if (index !== -1) { return list.slice(index + 1, index + 1 + (count || 0)); } }; // Find argument 'arg' in list, and if found, remove it as well as the 'count' items that follow it argfunc.remove = function(arg, count) { var index = list.indexOf(arg); if (index !== -1) { list.splice(index, (count || 0) + 1); } }; // Clone argument list argfunc.clone = function() { var cloned = utils.args(); cloned(list); return cloned; }; return argfunc; }, /** * Generate filter strings * * @param {String[]|Object[]} filters filter specifications. When using objects, * each must have the following properties: * @param {String} filters.filter filter name * @param {String|Array} [filters.inputs] (array of) input stream specifier(s) for the filter, * defaults to ffmpeg automatically choosing the first unused matching streams * @param {String|Array} [filters.outputs] (array of) output stream specifier(s) for the filter, * defaults to ffmpeg automatically assigning the output to the output file * @param {Object|String|Array} [filters.options] filter options, can be omitted to not set any options * @return String[] * @private */ makeFilterStrings: function(filters) { return filters.map(function(filterSpec) { if (typeof filterSpec === 'string') { return filterSpec; } var filterString = ''; // Filter string format is: // [input1][input2]...filter[output1][output2]... // The 'filter' part can optionaly have arguments: // filter=arg1:arg2:arg3 // filter=arg1=v1:arg2=v2:arg3=v3 // Add inputs if (Array.isArray(filterSpec.inputs)) { filterString += filterSpec.inputs.map(function(streamSpec) { return streamSpec.replace(streamRegexp, '[$1]'); }).join(''); } else if (typeof filterSpec.inputs === 'string') { filterString += filterSpec.inputs.replace(streamRegexp, '[$1]'); } // Add filter filterString += filterSpec.filter; // Add options if (filterSpec.options) { if (typeof filterSpec.options === 'string' || typeof filterSpec.options === 'number') { // Option string filterString += '=' + filterSpec.options; } else if (Array.isArray(filterSpec.options)) { // Option array (unnamed options) filterString += '=' + filterSpec.options.map(function(option) { if (typeof option === 'string' && option.match(filterEscapeRegexp)) { return '\'' + option + '\''; } else { return option; } }).join(':'); } else if (Object.keys(filterSpec.options).length) { // Option object (named options) filterString += '=' + Object.keys(filterSpec.options).map(function(option) { var value = filterSpec.options[option]; if (typeof value === 'string' && value.match(filterEscapeRegexp)) { value = '\'' + value + '\''; } return option + '=' + value; }).join(':'); } } // Add outputs if (Array.isArray(filterSpec.outputs)) { filterString += filterSpec.outputs.map(function(streamSpec) { return streamSpec.replace(streamRegexp, '[$1]'); }).join(''); } else if (typeof filterSpec.outputs === 'string') { filterString += filterSpec.outputs.replace(streamRegexp, '[$1]'); } return filterString; }); }, /** * Search for an executable * * Uses 'which' or 'where' depending on platform * * @param {String} name executable name * @param {Function} callback callback with signature (err, path) * @private */ which: function(name, callback) { if (name in whichCache) { return callback(null, whichCache[name]); } which(name, function(err, result){ if (err) { // Treat errors as not found return callback(null, whichCache[name] = ''); } callback(null, whichCache[name] = result); }); }, /** * Convert a [[hh:]mm:]ss[.xxx] timemark into seconds * * @param {String} timemark timemark string * @return Number * @private */ timemarkToSeconds: function(timemark) { if (typeof timemark === 'number') { return timemark; } if (timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) { return Number(timemark); } var parts = timemark.split(':'); // add seconds var secs = Number(parts.pop()); if (parts.length) { // add minutes secs += Number(parts.pop()) * 60; } if (parts.length) { // add hours secs += Number(parts.pop()) * 3600; } return secs; }, /** * Extract codec data from ffmpeg stderr and emit 'codecData' event if appropriate * Call it with an initially empty codec object once with each line of stderr output until it returns true * * @param {FfmpegCommand} command event emitter * @param {String} stderrLine ffmpeg stderr output line * @param {Object} codecObject object used to accumulate codec data between calls * @return {Boolean} true if codec data is complete (and event was emitted), false otherwise * @private */ extractCodecData: function(command, stderrLine, codecsObject) { var inputPattern = /Input #[0-9]+, ([^ ]+),/; var durPattern = /Duration\: ([^,]+)/; var audioPattern = /Audio\: (.*)/; var videoPattern = /Video\: (.*)/; if (!('inputStack' in codecsObject)) { codecsObject.inputStack = []; codecsObject.inputIndex = -1; codecsObject.inInput = false; } var inputStack = codecsObject.inputStack; var inputIndex = codecsObject.inputIndex; var inInput = codecsObject.inInput; var format, dur, audio, video; if (format = stderrLine.match(inputPattern)) { inInput = codecsObject.inInput = true; inputIndex = codecsObject.inputIndex = codecsObject.inputIndex + 1; inputStack[inputIndex] = { format: format[1], audio: '', video: '', duration: '' }; } else if (inInput && (dur = stderrLine.match(durPattern))) { inputStack[inputIndex].duration = dur[1]; } else if (inInput && (audio = stderrLine.match(audioPattern))) { audio = audio[1].split(', '); inputStack[inputIndex].audio = audio[0]; inputStack[inputIndex].audio_details = audio; } else if (inInput && (video = stderrLine.match(videoPattern))) { video = video[1].split(', '); inputStack[inputIndex].video = video[0]; inputStack[inputIndex].video_details = video; } else if (/Output #\d+/.test(stderrLine)) { inInput = codecsObject.inInput = false; } else if (/Stream mapping:|Press (\[q\]|ctrl-c) to stop/.test(stderrLine)) { command.emit.apply(command, ['codecData'].concat(inputStack)); return true; } return false; }, /** * Extract progress data from ffmpeg stderr and emit 'progress' event if appropriate * * @param {FfmpegCommand} command event emitter * @param {String} stderrLine ffmpeg stderr data * @private */ extractProgress: function(command, stderrLine) { var progress = parseProgressLine(stderrLine); if (progress) { // build progress report object var ret = { frames: parseInt(progress.frame, 10), currentFps: parseInt(progress.fps, 10), currentKbps: progress.bitrate ? parseFloat(progress.bitrate.replace('kbits/s', '')) : 0, targetSize: parseInt(progress.size || progress.Lsize, 10), timemark: progress.time }; // calculate percent progress using duration if (command._ffprobeData && command._ffprobeData.format && command._ffprobeData.format.duration) { var duration = Number(command._ffprobeData.format.duration); if (!isNaN(duration)) ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100; } command.emit('progress', ret); } }, /** * Extract error message(s) from ffmpeg stderr * * @param {String} stderr ffmpeg stderr data * @return {String} * @private */ extractError: function(stderr) { // Only return the last stderr lines that don't start with a space or a square bracket return stderr.split(nlRegexp).reduce(function(messages, message) { if (message.charAt(0) === ' ' || message.charAt(0) === '[') { return []; } else { messages.push(message); return messages; } }, []).join('\n'); }, /** * Creates a line ring buffer object with the following methods: * - append(str) : appends a string or buffer * - get() : returns the whole string * - close() : prevents further append() calls and does a last call to callbacks * - callback(cb) : calls cb for each line (incl. those already in the ring) * * @param {Number} maxLines maximum number of lines to store (<= 0 for unlimited) */ linesRing: function(maxLines) { var cbs = []; var lines = []; var current = null; var closed = false var max = maxLines - 1; function emit(line) { cbs.forEach(function(cb) { cb(line); }); } return { callback: function(cb) { lines.forEach(function(l) { cb(l); }); cbs.push(cb); }, append: function(str) { if (closed) return; if (str instanceof Buffer) str = '' + str; if (!str || str.length === 0) return; var newLines = str.split(nlRegexp); if (newLines.length === 1) { if (current !== null) { current = current + newLines.shift(); } else { current = newLines.shift(); } } else { if (current !== null) { current = current + newLines.shift(); emit(current); lines.push(current); } current = newLines.pop(); newLines.forEach(function(l) { emit(l); lines.push(l); }); if (max > -1 && lines.length > max) { lines.splice(0, lines.length - max); } } }, get: function() { if (current !== null) { return lines.concat([current]).join('\n'); } else { return lines.join('\n'); } }, close: function() { if (closed) return; if (current !== null) { emit(current); lines.push(current); if (max > -1 && lines.length > max) { lines.shift(); } current = null; } closed = true; } }; } }; ================================================ FILE: package.json ================================================ { "name": "fluent-ffmpeg", "version": "2.1.3", "description": "A fluent API to FFMPEG (http://www.ffmpeg.org)", "keywords": [ "ffmpeg" ], "author": "Stefan Schaermeli ", "contributors": [ { "name": "Felix Fichte", "email": "spruce@space-ships.de" } ], "license": "MIT", "bugs": { "mail": "schaermu@gmail.com", "url": "http://github.com/fluent-ffmpeg/node-fluent-ffmpeg/issues" }, "repository": "git://github.com/fluent-ffmpeg/node-fluent-ffmpeg.git", "devDependencies": { "jsdoc": "^4.0.0", "mocha": "^10.0.0", "nyc": "^15.1.0", "should": "^13.0.0" }, "dependencies": { "async": "^0.2.9", "which": "^1.1.1" }, "engines": { "node": ">=18" }, "main": "index", "scripts": { "test": "NODE_ENV=test nyc mocha --require should --reporter spec", "coverage": "nyc report --reporter=lcov" } } ================================================ FILE: test/aliases.test.js ================================================ /*jshint node:true*/ /*global describe,it*/ 'use strict'; var Ffmpeg = require('../index'); var aliases = { audio: { withNoAudio: ['noAudio'], withAudioCodec: ['audioCodec'], withAudioBitrate: ['audioBitrate'], withAudioChannels: ['audioChannels'], withAudioFrequency: ['audioFrequency'], withAudioQuality: ['audioQuality'], withAudioFilter: ['withAudioFilters','audioFilter','audioFilters'] }, custom: { addInputOption: ['addInputOptions','withInputOption','withInputOptions','inputOption','inputOptions'], addOutputOption: ['addOutputOptions','addOption','addOptions','withOutputOption','withOutputOptions','withOption','withOptions','outputOption','outputOptions'], complexFilter: ['filterGraph'] }, inputs: { addInput: ['input','mergeAdd'], fromFormat: ['withInputFormat','inputFormat'], withInputFps: ['withInputFPS','withFpsInput','withFPSInput','inputFPS','inputFps','fpsInput','FPSInput'], native: ['withNativeFramerate','nativeFramerate'], setStartTime: ['seekInput'] }, misc: { usingPreset: ['preset'] }, output: { addOutput: ['output'], withDuration: ['duration','setDuration'], toFormat: ['withOutputFormat','outputFormat','format'], seek: ['seekOutput'], updateFlvMetadata: ['flvmeta'] }, video: { withNoVideo: ['noVideo'], withVideoCodec: ['videoCodec'], withVideoBitrate: ['videoBitrate'], withVideoFilter: ['withVideoFilters','videoFilter','videoFilters'], withOutputFps: ['withOutputFPS','withFpsOutput','withFPSOutput','withFps','withFPS','outputFPS','outputFps','fpsOutput','FPSOutput','fps','FPS'], takeFrames: ['withFrames','frames'] }, videosize: { keepPixelAspect: ['keepDisplayAspect','keepDisplayAspectRatio','keepDAR'], withSize: ['setSize', 'size'], withAspect: ['withAspectRatio','setAspect','setAspectRatio','aspect','aspectRatio'], applyAutopadding: ['applyAutoPadding','applyAutopad','applyAutoPad','withAutopadding','withAutoPadding','withAutopad','withAutoPad','autoPad','autopad'] }, processing: { saveToFile: ['save'], writeToStream: ['stream', 'pipe'], run: ['exec', 'execute'], concat: ['concatenate', 'mergeToFile'], screenshots: ['screenshot', 'thumbnails', 'thumbnail', 'takeScreenshots'] } }; describe('Method aliases', function() { Object.keys(aliases).forEach(function(category) { describe(category + ' methods', function() { Object.keys(aliases[category]).forEach(function(method) { describe('FfmpegCommand#' + method, function() { aliases[category][method].forEach(function(alias) { it('should have a \'' + alias + '\' alias', function() { var ff = new Ffmpeg(); (typeof ff[method]).should.equal('function'); ff[method].should.equal(ff[alias]); }); }); }); }); }); }); }); ================================================ FILE: test/args.test.js ================================================ /*jshint node:true*/ /*global describe,it,before*/ 'use strict'; var Ffmpeg = require('../index'), utils = require('../lib/utils'), path = require('path'), fs = require('fs'), assert = require('assert'), exec = require('child_process').exec, testhelper = require('./helpers'); Ffmpeg.prototype._test_getArgs = function(callback) { var args; try { args = this._getArguments(); } catch(e) { return callback(null, e); } callback(args); }; Ffmpeg.prototype._test_getSizeFilters = function() { return utils.makeFilterStrings(this._currentOutput.sizeFilters.get()) .concat(this._currentOutput.videoFilters.get()); }; describe('Command', function() { before(function(done) { // check for ffmpeg installation this.testfile = path.join(__dirname, 'assets', 'testvideo-43.avi'); this.testfilewide = path.join(__dirname, 'assets', 'testvideo-169.avi'); var self = this; exec(testhelper.getFfmpegCheck(), function(err) { if (!err) { // check if file exists fs.exists(self.testfile, function(exists) { if (exists) { done(); } else { done(new Error('test video file does not exist, check path (' + self.testfile + ')')); } }); } else { done(new Error('cannot run test without ffmpeg installed, aborting test...')); } }); }); describe('Constructor', function() { it('should enable calling the constructor without new', function() { (Ffmpeg()).should.instanceof(Ffmpeg); }); }); describe('usingPreset', function() { it('should properly generate the command for the requested preset', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .usingPreset('podcast') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.length.should.equal(42); done(); }); }); it('should properly generate the command for the requested preset in custom folder', function(done) { new Ffmpeg({ source: this.testfile, nolog: true, preset: path.join(__dirname, 'assets', 'presets') }) .usingPreset('custompreset') ._test_getArgs(function(args) { args.length.should.equal(42); done(); }); }); it('should allow using functions as presets', function(done) { var presetArg; function presetFunc(command) { presetArg = command; command.withVideoCodec('libx264'); command.withAudioFrequency(22050); } var cmd = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }); cmd .usingPreset(presetFunc) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); presetArg.should.equal(cmd); args.join(' ').indexOf('-vcodec libx264').should.not.equal(-1); args.join(' ').indexOf('-ar 22050').should.not.equal(-1); done(); }); }); it('should throw an exception when a preset is not found', function() { var self = this; (function() { new Ffmpeg({ source: self.testfile, logger: testhelper.logger }) .usingPreset('NOTFOUND'); }).should.throw(/NOTFOUND could not be loaded/); }); it('should throw an exception when a preset has no load function', function() { (function() { new Ffmpeg({ presets: '../../lib' }).usingPreset('utils'); }).should.throw(/has no load\(\) function/); }); }); describe('withNoVideo', function() { it('should apply the skip video argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withNoVideo() ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-vn').should.above(-1); done(); }); }); it('should skip any video transformation options', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('320x?') .withNoVideo() .withAudioBitrate('256k') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-vn').should.above(-1); args.indexOf('-s').should.equal(-1); args.indexOf('-b:a').should.above(-1); done(); }); }); }); describe('withNoAudio', function() { it('should apply the skip audio argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withNoAudio() ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-an').should.above(-1); done(); }); }); it('should skip any audio transformation options', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioChannels(2) .withNoAudio() .withSize('320x?') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-an').should.above(-1); args.indexOf('-ac').should.equal(-1); args.indexOf('scale=w=320:h=trunc(ow/a/2)*2').should.above(-1); done(); }); }); }); describe('withVideoBitrate', function() { it('should apply default bitrate argument by default', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoBitrate('256k') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-b:v').should.above(-1); done(); }); }); it('should apply additional bitrate arguments for constant bitrate', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoBitrate('256k', true) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-b:v').should.above(-1); args.indexOf('-maxrate').should.above(-1); args.indexOf('-minrate').should.above(-1); args.indexOf('-bufsize').should.above(-1); done(); }); }); }); describe('withMultiFile', function() { it('should allow image2 multi-file input format', function(done) { new Ffmpeg({ source: 'image-%05d.png', logger: testhelper.logger }) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-i').should.above(-1); args.indexOf('image-%05d.png').should.above(-1); done(); }); }); }); describe('withFps', function() { it('should apply the rate argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withFps(27.77) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-r').should.above(-1); args.indexOf(27.77).should.above(-1); done(); }); }); }); describe('withInputFPS', function() { it('should apply the rate argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withInputFPS(27.77) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-r').should.above(-1).and.below(args.indexOf('-i')); args.indexOf(27.77).should.above(-1).and.below(args.indexOf('-i')); done(); }); }); }); describe('native', function() { it('should apply the native framerate argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .native() ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-re').should.above(-1).and.below(args.indexOf('-i')); done(); }); }); }); describe('addingAdditionalInput', function() { it('should allow for additional inputs', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .addInput('soundtrack.mp3') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-i').should.above(-1); args.indexOf('soundtrack.mp3').should.above(-1); done(); }); }); it('should fail to add invalid inputs', function() { (function() { new Ffmpeg().addInput({}); }).should.throw(/Invalid input/); }); it('should refuse to add more than 1 input stream', function() { var stream1 = fs.createReadStream(this.testfile); var stream2 = fs.createReadStream(this.testfilewide); var command = new Ffmpeg().addInput(stream1); (function() { command.addInput(stream2); }).should.throw(/Only one input stream is supported/); }); it('should fail on input-related options when no input was added', function() { (function() { new Ffmpeg().inputFormat('avi'); }).should.throw(/No input specified/); (function() { new Ffmpeg().inputFps(24); }).should.throw(/No input specified/); (function() { new Ffmpeg().seekInput(1); }).should.throw(/No input specified/); (function() { new Ffmpeg().loop(); }).should.throw(/No input specified/); (function() { new Ffmpeg().inputOptions('-anoption'); }).should.throw(/No input specified/); }); }); describe('withVideoCodec', function() { it('should apply the video codec argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoCodec('libx264') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-vcodec').should.above(-1); args.indexOf('libx264').should.above(-1); done(); }); }); }); describe('withVideoFilter', function() { it('should apply the video filter argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoFilter('scale=123:456') .withVideoFilter('pad=1230:4560:100:100:yellow') .withVideoFilter('multiple=1', 'filters=2') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:v').should.above(-1); args.indexOf('scale=123:456,pad=1230:4560:100:100:yellow,multiple=1,filters=2').should.above(-1); done(); }); }); it('should accept filter arrays', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoFilter(['multiple=1', 'filters=2']) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:v').should.above(-1); args.indexOf('multiple=1,filters=2').should.above(-1); done(); }); }); it('should enable using filter objects', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withVideoFilter( { filter: 'option_string', options: 'opt1=value1:opt2=value2' }, { filter: 'unnamed_options', options: ['opt1', 'opt2'] }, { filter: 'named_options', options: { opt1: 'value1', opt2: 'value2' } } ) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:v').should.above(-1); args.indexOf('option_string=opt1=value1:opt2=value2,unnamed_options=opt1:opt2,named_options=opt1=value1:opt2=value2').should.above(-1); done(); }); }); }); describe('withAudioBitrate', function() { it('should apply the audio bitrate argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioBitrate(256) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-b:a').should.above(-1); args.indexOf('256k').should.above(-1); done(); }); }); }); describe('loop', function() { it('should add the -loop 1 argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .loop() ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); if(args.indexOf('-loop') != -1 || args.indexOf('-loop_output') != -1){ done(); } else{ done(new Error('args should contain loop or loop_output')); } }); }); it('should add the -loop 1 and a time argument (seconds)', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .loop(120) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); if(args.indexOf('-loop') != -1 || args.indexOf('-loop_output') != -1){ args.indexOf('-t').should.above(-1); args.indexOf(120).should.above(-1); done(); } else{ done(new Error('args should contain loop or loop_output')); } }); }); it('should add the -loop 1 and a time argument (timemark)', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .loop('00:06:46.81') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); if(args.indexOf('-loop') != -1 || args.indexOf('-loop_output') != -1){ args.indexOf('-t').should.above(-1); args.indexOf('00:06:46.81').should.above(-1); done(); } else{ done(new Error('args should contain loop or loop_output')); } }); }); }); describe('takeFrames', function() { it('should add the -vframes argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .takeFrames(250) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-vframes').should.above(-1); args.indexOf(250).should.above(-1); done(); }); }); }); describe('withAudioCodec', function() { it('should apply the audio codec argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioCodec('mp3') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-acodec').should.above(-1); args.indexOf('mp3').should.above(-1); done(); }); }); }); describe('withAudioFilter', function() { it('should apply the audio filter argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioFilter('silencedetect=n=-50dB:d=5') .withAudioFilter('volume=0.5') .withAudioFilter('multiple=1', 'filters=2') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:a').should.above(-1); args.indexOf('silencedetect=n=-50dB:d=5,volume=0.5,multiple=1,filters=2').should.above(-1); done(); }); }); it('should accept filter arrays', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioFilter(['multiple=1', 'filters=2']) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:a').should.above(-1); args.indexOf('multiple=1,filters=2').should.above(-1); done(); }); }); it('should enable using filter objects', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioFilter( { filter: 'option_string', options: 'opt1=value1:opt2=value2' }, { filter: 'unnamed_options', options: ['opt1', 'opt2'] }, { filter: 'named_options', options: { opt1: 'value1', opt2: 'value2' } } ) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-filter:a').should.above(-1); args.indexOf('option_string=opt1=value1:opt2=value2,unnamed_options=opt1:opt2,named_options=opt1=value1:opt2=value2').should.above(-1); done(); }); }); }); describe('withAudioChannels', function() { it('should apply the audio channels argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioChannels(1) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-ac').should.above(-1); args.indexOf(1).should.above(-1); done(); }); }); }); describe('withAudioFrequency', function() { it('should apply the audio frequency argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioFrequency(22500) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-ar').should.above(-1); args.indexOf(22500).should.above(-1); done(); }); }); }); describe('withAudioQuality', function() { it('should apply the audio quality argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAudioQuality(5) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-aq').should.above(-1); args.indexOf(5).should.above(-1); done(); }); }); }); describe('setStartTime', function() { it('should apply the start time offset argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .setStartTime('00:00:10') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-ss').should.above(-1).and.below(args.indexOf('-i')); args.indexOf('00:00:10').should.above(args.indexOf('-ss')).and.below(args.indexOf('-i')); done(); }); }); }); describe('setDuration', function() { it('should apply the record duration argument', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .setDuration(10) ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-t').should.above(-1); args.indexOf(10).should.above(-1); done(); }); }); }); describe('addOption(s)', function() { it('should apply a single option', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .addOption('-ab', '256k') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-ab').should.above(-1); args.indexOf('256k').should.above(-1); done(); }); }); it('should apply supplied extra options', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .addOptions(['-flags', '+loop', '-cmp', '+chroma', '-partitions','+parti4x4+partp8x8+partb8x8']) .addOptions('-single option') .addOptions('-multiple', '-options') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-flags').should.above(-1); args.indexOf('+loop').should.above(-1); args.indexOf('-cmp').should.above(-1); args.indexOf('+chroma').should.above(-1); args.indexOf('-partitions').should.above(-1); args.indexOf('+parti4x4+partp8x8+partb8x8').should.above(-1); args.indexOf('-single').should.above(-1); args.indexOf('option').should.above(-1); args.indexOf('-multiple').should.above(-1); args.indexOf('-options').should.above(-1); done(); }); }); it('should apply a single input option', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .addInputOption('-r', '29.97') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); var joined = args.join(' '); joined.indexOf('-r 29.97').should.above(-1).and.below(joined.indexOf('-i ')); done(); }); }); it('should apply multiple input options', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .addInputOptions(['-r 29.97', '-f ogg']) .addInputOptions('-single option') .addInputOptions('-multiple', '-options') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); var joined = args.join(' '); joined.indexOf('-r 29.97').should.above(-1).and.below(joined.indexOf('-i')); joined.indexOf('-f ogg').should.above(-1).and.below(joined.indexOf('-i')); joined.indexOf('-single option').should.above(-1).and.below(joined.indexOf('-i')); joined.indexOf('-multiple').should.above(-1).and.below(joined.indexOf('-i')); joined.indexOf('-options').should.above(-1).and.below(joined.indexOf('-i')); done(); }); }); }); describe('toFormat', function() { it('should apply the target format', function(done) { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .toFormat('mp4') ._test_getArgs(function(args, err) { testhelper.logArgError(err); assert.ok(!err); args.indexOf('-f').should.above(-1); args.indexOf('mp4').should.above(-1); done(); }); }); }); describe('Size calculations', function() { it('Should throw an error when an invalid aspect ratio is passed', function() { (function() { new Ffmpeg().aspect('blah'); }).should.throw(/Invalid aspect ratio/); }); it('Should add scale and setsar filters when keepPixelAspect was called', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .keepPixelAspect(true) ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(sar,1),iw*sar,iw)\':h=\'if(lt(sar,1),ih/sar,ih)\''); filters[1].should.equal('setsar=1'); }); it('Should throw an error when an invalid size was requested', function() { (function() { new Ffmpeg().withSize('aslkdbasd'); }).should.throw(/^Invalid size specified/); }); it('Should not add scale filters when withSize was not called', function() { new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) ._test_getSizeFilters().length.should.equal(0); new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withAspect(4/3) ._test_getSizeFilters().length.should.equal(0); new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .applyAutopadding(true, 'white') ._test_getSizeFilters().length.should.equal(0); }); it('Should add proper scale filter when withSize was called with a percent value', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('42%') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=trunc(iw*0.42/2)*2:h=trunc(ih*0.42/2)*2'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('42%') .withAspect(4/3) ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=trunc(iw*0.42/2)*2:h=trunc(ih*0.42/2)*2'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('42%') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=trunc(iw*0.42/2)*2:h=trunc(ih*0.42/2)*2'); }); it('Should add proper scale filter when withSize was called with a fixed size', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x200') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=100:h=200'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x200') .withAspect(4/3) ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=100:h=200'); }); it('Should add proper scale filter when withSize was called with a "?" and no aspect ratio is specified', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=100:h=trunc(ow/a/2)*2'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=100:h=trunc(ow/a/2)*2'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('?x200') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=trunc(oh*a/2)*2:h=200'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('?x200') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=trunc(oh*a/2)*2:h=200'); }); it('Should add proper scale filter when withSize was called with a "?" and an aspect ratio is specified', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .withAspect(0.5) ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=100:h=200'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('?x100') .withAspect(2) ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=200:h=100'); }); it('Should add scale and pad filters when withSize was called with a "?", aspect ratio and auto padding are specified', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .withAspect(0.5) .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,0.5),100,trunc(200*a/2)*2)\':h=\'if(lt(a,0.5),200,trunc(100/a/2)*2)\''); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=white'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('?x100') .withAspect(2) .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,2),200,trunc(100*a/2)*2)\':h=\'if(lt(a,2),100,trunc(200/a/2)*2)\''); filters[1].should.equal('pad=w=200:h=100:x=\'if(gt(a,2),0,(200-iw)/2)\':y=\'if(lt(a,2),0,(100-ih)/2)\':color=white'); }); it('Should add scale and pad filters when withSize was called with a fixed size and auto padding is specified', function() { var filters; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x200') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,0.5),100,trunc(200*a/2)*2)\':h=\'if(lt(a,0.5),200,trunc(100/a/2)*2)\''); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=white'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x200') .withAspect(4/3) .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,0.5),100,trunc(200*a/2)*2)\':h=\'if(lt(a,0.5),200,trunc(100/a/2)*2)\''); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=white'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('200x100') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,2),200,trunc(100*a/2)*2)\':h=\'if(lt(a,2),100,trunc(200/a/2)*2)\''); filters[1].should.equal('pad=w=200:h=100:x=\'if(gt(a,2),0,(200-iw)/2)\':y=\'if(lt(a,2),0,(100-ih)/2)\':color=white'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('200x100') .withAspect(4/3) .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,2),200,trunc(100*a/2)*2)\':h=\'if(lt(a,2),100,trunc(200/a/2)*2)\''); filters[1].should.equal('pad=w=200:h=100:x=\'if(gt(a,2),0,(200-iw)/2)\':y=\'if(lt(a,2),0,(100-ih)/2)\':color=white'); }); it('Should round sizes to multiples of 2', function() { var filters; var aspect = 102/202; filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('101x201') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=102:h=202'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('101x201') .applyAutopadding(true, 'white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[0].should.equal('scale=w=\'if(gt(a,' + aspect + '),102,trunc(202*a/2)*2)\':h=\'if(lt(a,' + aspect + '),202,trunc(102/a/2)*2)\''); filters[1].should.equal('pad=w=102:h=202:x=\'if(gt(a,' + aspect + '),0,(102-iw)/2)\':y=\'if(lt(a,' + aspect + '),0,(202-ih)/2)\':color=white'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('101x?') .withAspect('1:2') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=102:h=202'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('?x201') .withAspect('1:2') ._test_getSizeFilters(); filters.length.should.equal(1); filters[0].should.equal('scale=w=102:h=202'); }); it('Should apply autopadding when no boolean argument was passed to applyAutopadding', function() { var filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .withAspect(0.5) .applyAutopadding('white') ._test_getSizeFilters(); filters.length.should.equal(2); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=white'); }); it('Should default to black padding', function() { var filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .withAspect(0.5) .applyAutopadding() ._test_getSizeFilters(); filters.length.should.equal(2); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=black'); filters = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .withSize('100x?') .withAspect(0.5) .applyAutopadding(true) ._test_getSizeFilters(); filters.length.should.equal(2); filters[1].should.equal('pad=w=100:h=200:x=\'if(gt(a,0.5),0,(100-iw)/2)\':y=\'if(lt(a,0.5),0,(200-ih)/2)\':color=black'); }); }); describe('complexFilter', function() { it('should generate a complex filter from a single filter', function() { var filters = new Ffmpeg() .complexFilter('filterstring') ._getArguments(); filters.length.should.equal(2); filters[0].should.equal('-filter_complex'); filters[1].should.equal('filterstring'); }); it('should generate a complex filter from a filter array', function() { var filters = new Ffmpeg() .complexFilter(['filter1', 'filter2']) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('filter1;filter2'); }); it('should support filter objects', function() { var filters = new Ffmpeg() .complexFilter([ 'filter1', { filter: 'filter2' } ]) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('filter1;filter2'); }); it('should support filter options', function() { var filters = new Ffmpeg() .complexFilter([ { filter: 'filter1', options: 'optionstring' }, { filter: 'filter2', options: ['opt1', 'opt2', 'opt3'] }, { filter: 'filter3', options: { opt1: 'value1', opt2: 'value2' } } ]) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('filter1=optionstring;filter2=opt1:opt2:opt3;filter3=opt1=value1:opt2=value2'); }); it('should escape filter options with ambiguous characters', function() { var filters = new Ffmpeg() .complexFilter([ { filter: 'filter1', options: 'optionstring' }, { filter: 'filter2', options: ['op,t1', 'op,t2', 'op,t3'] }, { filter: 'filter3', options: { opt1: 'val,ue1', opt2: 'val,ue2' } } ]) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('filter1=optionstring;filter2=\'op,t1\':\'op,t2\':\'op,t3\';filter3=opt1=\'val,ue1\':opt2=\'val,ue2\''); }); it('should support filter input streams', function() { var filters = new Ffmpeg() .complexFilter([ { filter: 'filter1', inputs: 'input' }, { filter: 'filter2', inputs: '[input]' }, { filter: 'filter3', inputs: ['[input1]', 'input2'] } ]) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('[input]filter1;[input]filter2;[input1][input2]filter3'); }); it('should support filter output streams', function() { var filters = new Ffmpeg() .complexFilter([ { filter: 'filter1', options: 'opt', outputs: 'output' }, { filter: 'filter2', options: 'opt', outputs: '[output]' }, { filter: 'filter3', options: 'opt', outputs: ['[output1]', 'output2'] } ]) ._getArguments(); filters.length.should.equal(2); filters[1].should.equal('filter1=opt[output];filter2=opt[output];filter3=opt[output1][output2]'); }); it('should support an additional mapping argument', function() { var filters = new Ffmpeg() .complexFilter(['filter1', 'filter2'], 'output') ._getArguments(); filters.length.should.equal(4); filters[2].should.equal('-map'); filters[3].should.equal('[output]'); filters = new Ffmpeg() .complexFilter(['filter1', 'filter2'], '[output]') ._getArguments(); filters.length.should.equal(4); filters[2].should.equal('-map'); filters[3].should.equal('[output]'); filters = new Ffmpeg() .complexFilter(['filter1', 'filter2'], ['[output1]', 'output2']) ._getArguments(); filters.length.should.equal(6); filters[2].should.equal('-map'); filters[3].should.equal('[output1]'); filters[4].should.equal('-map'); filters[5].should.equal('[output2]'); }); it('should override any previously set complex filtergraphs', function() { var filters = new Ffmpeg() .complexFilter(['filter1a', 'filter1b'], 'output1') .complexFilter(['filter2a', 'filter2b'], 'output2') ._getArguments(); filters.length.should.equal(4); filters[1].should.equal('filter2a;filter2b'); filters[2].should.equal('-map'); filters[3].should.equal('[output2]'); }); }); describe('clone', function() { it('should return a new FfmpegCommand instance', function() { var command = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }); var clone = command.clone(); clone.should.instanceof(Ffmpeg); clone.should.not.equal(command); }); it('should duplicate FfmpegCommand options at the time of the call', function(done) { var command = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .preset('flashvideo'); var clone = command.clone(); command._test_getArgs(function(originalArgs) { clone._test_getArgs(function(cloneArgs) { cloneArgs.length.should.equal(originalArgs.length); originalArgs.forEach(function(arg, index) { cloneArgs[index].should.equal(arg); }); done(); }); }); }); it('should have separate argument lists', function(done) { var command = new Ffmpeg({ source: this.testfile, logger: testhelper.logger }) .preset('flashvideo'); var clone = command.clone().audioFrequency(22050); command._test_getArgs(function(originalArgs) { clone._test_getArgs(function(cloneArgs) { cloneArgs.length.should.equal(originalArgs.length + 2); done(); }); }); }); }); }); ================================================ FILE: test/assets/ffserver.conf ================================================ Port 8090 BindAddress 127.0.0.1 RTSPPort 5540 RTSPBindAddress 127.0.0.1 MaxHTTPConnections 1000 MaxClients 10 MaxBandwidth 1000000 ReadOnlyFile teststream.ffm ACL allow 127.0.0.1 Format mpegts AudioCodec mp2 VideoCodec libx264 ReadOnlyFile teststream.ffm ACL allow 127.0.0.1 Format rtp File testinput.ffm ACL allow 127.0.0.1 Format mpegts AudioCodec mp2 VideoCodec libx264 ================================================ FILE: test/assets/presets/custompreset.js ================================================ exports.load = function(ffmpeg) { ffmpeg .toFormat('m4v') .withVideoBitrate('512k') .withVideoCodec('libx264') .withSize('320x176') .withAudioBitrate('128k') .withAudioCodec('aac') .withAudioChannels(1) .addOptions(['-flags', '+loop', '-cmp', '+chroma', '-partitions','+parti4x4+partp8x8+partb8x8', '-flags2', '+mixed_refs', '-me_method umh', '-subq 5', '-bufsize 2M', '-rc_eq \'blurCplx^(1-qComp)\'', '-qcomp 0.6', '-qmin 10', '-qmax 51', '-qdiff 4', '-level 13' ]); return ffmpeg; }; ================================================ FILE: test/capabilities.test.js ================================================ /*jshint node:true*/ /*global describe,it,beforeEach,afterEach,after*/ 'use strict'; var Ffmpeg = require('../index'), path = require('path'), assert = require('assert'), testhelper = require('./helpers'), async = require('async'); // delimiter fallback for node 0.8 var PATH_DELIMITER = path.delimiter || (require('os').platform().match(/win(32|64)/) ? ';' : ':'); describe('Capabilities', function() { describe('ffmpeg capabilities', function() { it('should enable querying for available codecs', function(done) { new Ffmpeg({ source: '' }).getAvailableCodecs(function(err, codecs) { testhelper.logError(err); assert.ok(!err); (typeof codecs).should.equal('object'); Object.keys(codecs).length.should.not.equal(0); ('pcm_s16le' in codecs).should.equal(true); ('type' in codecs.pcm_s16le).should.equal(true); (typeof codecs.pcm_s16le.type).should.equal('string'); ('description' in codecs.pcm_s16le).should.equal(true); (typeof codecs.pcm_s16le.description).should.equal('string'); ('canEncode' in codecs.pcm_s16le).should.equal(true); (typeof codecs.pcm_s16le.canEncode).should.equal('boolean'); ('canDecode' in codecs.pcm_s16le).should.equal(true); (typeof codecs.pcm_s16le.canDecode).should.equal('boolean'); done(); }); }); it('should enable querying for available encoders', function(done) { new Ffmpeg({ source: '' }).getAvailableEncoders(function(err, encoders) { testhelper.logError(err); assert.ok(!err); (typeof encoders).should.equal('object'); Object.keys(encoders).length.should.not.equal(0); ('pcm_s16le' in encoders).should.equal(true); ('type' in encoders.pcm_s16le).should.equal(true); (typeof encoders.pcm_s16le.type).should.equal('string'); ('description' in encoders.pcm_s16le).should.equal(true); (typeof encoders.pcm_s16le.description).should.equal('string'); ('experimental' in encoders.pcm_s16le).should.equal(true); (typeof encoders.pcm_s16le.experimental).should.equal('boolean'); done(); }); }); it('should enable querying for available formats', function(done) { new Ffmpeg({ source: '' }).getAvailableFormats(function(err, formats) { testhelper.logError(err); assert.ok(!err); (typeof formats).should.equal('object'); Object.keys(formats).length.should.not.equal(0); ('wav' in formats).should.equal(true); ('description' in formats.wav).should.equal(true); (typeof formats.wav.description).should.equal('string'); ('canMux' in formats.wav).should.equal(true); (typeof formats.wav.canMux).should.equal('boolean'); ('canDemux' in formats.wav).should.equal(true); (typeof formats.wav.canDemux).should.equal('boolean'); done(); }); }); it('should enable querying for available filters', function(done) { new Ffmpeg({ source: '' }).getAvailableFilters(function(err, filters) { testhelper.logError(err); assert.ok(!err); (typeof filters).should.equal('object'); Object.keys(filters).length.should.not.equal(0); ('anull' in filters).should.equal(true); ('description' in filters.anull).should.equal(true); (typeof filters.anull.description).should.equal('string'); ('input' in filters.anull).should.equal(true); (typeof filters.anull.input).should.equal('string'); ('output' in filters.anull).should.equal(true); (typeof filters.anull.output).should.equal('string'); ('multipleInputs' in filters.anull).should.equal(true); (typeof filters.anull.multipleInputs).should.equal('boolean'); ('multipleOutputs' in filters.anull).should.equal(true); (typeof filters.anull.multipleOutputs).should.equal('boolean'); done(); }); }); it('should enable querying capabilities without instanciating a command', function(done) { Ffmpeg.getAvailableCodecs(function(err, codecs) { testhelper.logError(err); assert.ok(!err); (typeof codecs).should.equal('object'); Object.keys(codecs).length.should.not.equal(0); Ffmpeg.getAvailableFilters(function(err, filters) { testhelper.logError(err); assert.ok(!err); (typeof filters).should.equal('object'); Object.keys(filters).length.should.not.equal(0); Ffmpeg.getAvailableFormats(function(err, formats) { testhelper.logError(err); assert.ok(!err); (typeof formats).should.equal('object'); Object.keys(formats).length.should.not.equal(0); done(); }); }); }); }); it('should enable checking command arguments for available codecs, formats and encoders', function(done) { async.waterfall([ // Check with everything available function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') .audioCodec('pcm_u16le') .videoCodec('png') .toFormat('mp4') ._checkCapabilities(cb); }, // Invalid input format function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('invalid-input-format') .audioCodec('pcm_u16le') .videoCodec('png') .toFormat('mp4') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Input format invalid-input-format is not available/); cb(); }); }, // Invalid output format function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') .audioCodec('pcm_u16le') .videoCodec('png') .toFormat('invalid-output-format') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Output format invalid-output-format is not available/); cb(); }); }, // Invalid audio codec function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') .audioCodec('invalid-audio-codec') .videoCodec('png') .toFormat('mp4') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Audio codec invalid-audio-codec is not available/); cb(); }); }, // Invalid video codec function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') .audioCodec('pcm_u16le') .videoCodec('invalid-video-codec') .toFormat('mp4') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Video codec invalid-video-codec is not available/); cb(); }); }, // Invalid audio encoder function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') // Valid codec, but not a valid encoder for audio .audioCodec('png') .videoCodec('png') .toFormat('mp4') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Audio codec png is not available/); cb(); }); }, // Invalid video encoder function(cb) { new Ffmpeg('/path/to/file.avi') .fromFormat('avi') .audioCodec('pcm_u16le') // Valid codec, but not a valid encoder for video .videoCodec('pcm_u16le') .toFormat('mp4') ._checkCapabilities(function(err) { assert.ok(!!err); err.message.should.match(/Video codec pcm_u16le is not available/); cb(); }); } ], function(err) { testhelper.logError(err); assert.ok(!err); done(); }); }); it('should check capabilities before running a command', function(done) { new Ffmpeg('/path/to/file.avi') .on('error', function(err) { err.message.should.match(/Output format invalid-output-format is not available/); done(); }) .toFormat('invalid-output-format') .saveToFile('/tmp/will-not-be-created.mp4'); }); }); describe('ffmpeg path', function() { var FFMPEG_PATH; var ALT_FFMPEG_PATH; var skipAltTest = false; // Only test with FFMPEG_PATH when we actually have an alternative path if (process.env.ALT_FFMPEG_PATH) { ALT_FFMPEG_PATH = process.env.ALT_FFMPEG_PATH; } else { skipAltTest = true; } beforeEach(function() { // Save environment before each test FFMPEG_PATH = process.env.FFMPEG_PATH; }); afterEach(function() { // Restore environment after each test process.env.FFMPEG_PATH = FFMPEG_PATH; }); after(function() { // Forget paths after all tests (new Ffmpeg())._forgetPaths(); }); it('should allow manual definition of ffmpeg binary path', function(done) { var ff = new Ffmpeg(); ff.setFfmpegPath('/doom/di/dom'); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.equal('/doom/di/dom'); done(); }); }); it('should allow static manual definition of ffmpeg binary path', function(done) { var ff = new Ffmpeg(); Ffmpeg.setFfmpegPath('/doom/di/dom2'); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.equal('/doom/di/dom2'); done(); }); }); it('should look for ffmpeg in the PATH if FFMPEG_PATH is not defined', function(done) { var ff = new Ffmpeg(); delete process.env.FFMPEG_PATH; ff._forgetPaths(); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.instanceOf(String); ffmpeg.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(ffmpeg)).should.above(-1); done(); }); }); (skipAltTest ? it.skip : it)('should use FFMPEG_PATH if defined and valid', function(done) { var ff = new Ffmpeg(); process.env.FFMPEG_PATH = ALT_FFMPEG_PATH; ff._forgetPaths(); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.equal(ALT_FFMPEG_PATH); done(); }); }); it('should fall back to searching in the PATH if FFMPEG_PATH is invalid', function(done) { var ff = new Ffmpeg(); process.env.FFMPEG_PATH = '/nope/not-here/nothing-to-see-here'; ff._forgetPaths(); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.instanceOf(String); ffmpeg.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(ffmpeg)).should.above(-1); done(); }); }); it('should remember ffmpeg path', function(done) { var ff = new Ffmpeg(); delete process.env.FFMPEG_PATH; ff._forgetPaths(); ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.instanceOf(String); ffmpeg.length.should.above(0); // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; ff._getFfmpegPath(function(err, ffmpeg) { testhelper.logError(err); assert.ok(!err); ffmpeg.should.instanceOf(String); ffmpeg.length.should.above(0); after.should.equal(0); done(); }); after = 1; }); }); }); describe('ffprobe path', function() { var FFPROBE_PATH; var ALT_FFPROBE_PATH; var skipAltTest = false; // Only test with FFPROBE_PATH when we actually have an alternative path if (process.env.ALT_FFPROBE_PATH) { ALT_FFPROBE_PATH = process.env.ALT_FFPROBE_PATH; } else { skipAltTest = true; } beforeEach(function() { // Save environment before each test FFPROBE_PATH = process.env.FFPROBE_PATH; }); afterEach(function() { // Restore environment after each test process.env.FFPROBE_PATH = FFPROBE_PATH; }); after(function() { // Forget paths after all tests (new Ffmpeg())._forgetPaths(); }); it('should allow manual definition of ffprobe binary path', function(done) { var ff = new Ffmpeg(); ff.setFfprobePath('/doom/di/dom'); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.equal('/doom/di/dom'); done(); }); }); it('should allow static manual definition of ffprobe binary path', function(done) { var ff = new Ffmpeg(); Ffmpeg.setFfprobePath('/doom/di/dom2'); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.equal('/doom/di/dom2'); done(); }); }); it('should look for ffprobe in the PATH if FFPROBE_PATH is not defined', function(done) { var ff = new Ffmpeg(); delete process.env.FFPROBE_PATH; ff._forgetPaths(); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.instanceOf(String); ffprobe.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(ffprobe)).should.above(-1); done(); }); }); (skipAltTest ? it.skip : it)('should use FFPROBE_PATH if defined and valid', function(done) { var ff = new Ffmpeg(); process.env.FFPROBE_PATH = ALT_FFPROBE_PATH; ff._forgetPaths(); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.equal(ALT_FFPROBE_PATH); done(); }); }); it('should fall back to searching in the PATH if FFPROBE_PATH is invalid', function(done) { var ff = new Ffmpeg(); process.env.FFPROBE_PATH = '/nope/not-here/nothing-to-see-here'; ff._forgetPaths(); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.instanceOf(String); ffprobe.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(ffprobe)).should.above(-1); done(); }); }); it('should remember ffprobe path', function(done) { var ff = new Ffmpeg(); delete process.env.FFPROBE_PATH; ff._forgetPaths(); ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.instanceOf(String); ffprobe.length.should.above(0); // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; ff._getFfprobePath(function(err, ffprobe) { testhelper.logError(err); assert.ok(!err); ffprobe.should.instanceOf(String); ffprobe.length.should.above(0); after.should.equal(0); done(); }); after = 1; }); }); }); describe('flvtool path', function() { var FLVTOOL2_PATH; var ALT_FLVTOOL_PATH; var skipAltTest = false; var skipTest = false; if (process.env.FLVTOOL2_PRESENT === 'no') { skipTest = true; } // Only test with FLVTOOL2_PATH when we actually have an alternative path if (process.env.ALT_FLVTOOL_PATH) { ALT_FLVTOOL_PATH = process.env.ALT_FLVTOOL_PATH; } else { skipAltTest = true; } beforeEach(function() { // Save environment before each test FLVTOOL2_PATH = process.env.FLVTOOL2_PATH; }); afterEach(function() { // Restore environment after each test process.env.FLVTOOL2_PATH = FLVTOOL2_PATH; }); after(function() { // Forget paths after all tests (new Ffmpeg())._forgetPaths(); }); (skipTest ? it.skip : it)('should allow manual definition of fflvtool binary path', function(done) { var ff = new Ffmpeg(); ff.setFlvtoolPath('/doom/di/dom'); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.equal('/doom/di/dom'); done(); }); }); (skipTest ? it.skip : it)('should allow static manual definition of fflvtool binary path', function(done) { var ff = new Ffmpeg(); Ffmpeg.setFlvtoolPath('/doom/di/dom2'); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.equal('/doom/di/dom2'); done(); }); }); (skipTest ? it.skip : it)('should look for fflvtool in the PATH if FLVTOOL2_PATH is not defined', function(done) { var ff = new Ffmpeg(); delete process.env.FLVTOOL2_PATH; ff._forgetPaths(); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.instanceOf(String); fflvtool.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(fflvtool)).should.above(-1); done(); }); }); (skipTest || skipAltTest ? it.skip : it)('should use FLVTOOL2_PATH if defined and valid', function(done) { var ff = new Ffmpeg(); process.env.FLVTOOL2_PATH = ALT_FLVTOOL_PATH; ff._forgetPaths(); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.equal(ALT_FLVTOOL_PATH); done(); }); }); (skipTest ? it.skip : it)('should fall back to searching in the PATH if FLVTOOL2_PATH is invalid', function(done) { var ff = new Ffmpeg(); process.env.FLVTOOL2_PATH = '/nope/not-here/nothing-to-see-here'; ff._forgetPaths(); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.instanceOf(String); fflvtool.length.should.above(0); var paths = process.env.PATH.split(PATH_DELIMITER); paths.indexOf(path.dirname(fflvtool)).should.above(-1); done(); }); }); (skipTest ? it.skip : it)('should remember fflvtool path', function(done) { var ff = new Ffmpeg(); delete process.env.FLVTOOL2_PATH; ff._forgetPaths(); ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.instanceOf(String); fflvtool.length.should.above(0); // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; ff._getFlvtoolPath(function(err, fflvtool) { testhelper.logError(err); assert.ok(!err); fflvtool.should.instanceOf(String); fflvtool.length.should.above(0); after.should.equal(0); done(); }); after = 1; }); }); }); }); ================================================ FILE: test/helpers.js ================================================ /*jshint node:true*/ 'use strict'; var TestHelpers; exports = module.exports = TestHelpers = { getFfmpegCheck: function() { var platform = require('os').platform(); if (!platform.match(/win(32|64)/)) { // linux/mac, use which return 'which ffmpeg'; } else { // windows, use where (> windows server 2003 / windows 7) return 'where /Q ffmpeg'; } }, logger: { debug: function(arg) { if (process.env.FLUENTFFMPEG_COV !== '1') console.log(' [DEBUG] ' + arg); }, info: function(arg) { if (process.env.FLUENTFFMPEG_COV !== '1') console.log(' [INFO] ' + arg); }, warn: function(arg) { if (process.env.FLUENTFFMPEG_COV !== '1') console.log(' [WARN] ' + arg); }, error: function(arg) { if (process.env.FLUENTFFMPEG_COV !== '1') console.log(' [ERROR] ' + arg); } }, logArgError: function(err) { if (err) { console.log('got error: ' + (err.stack || err)); if (err.ffmpegOut) { console.log('---stdout---'); console.log(err.ffmpegOut); } if (err.ffmpegErr) { console.log('---stderr---'); console.log(err.ffmpegErr); } if (err.spawnErr) { console.log('---spawn error---'); console.log(err.spawnErr.stack || err.spawnErr); } } }, logError: function(err, stdout, stderr) { if (err) { console.log('got error: ' + (err.stack || err)); if (err.ffmpegOut) { console.log('---metadata stdout---'); console.log(err.ffmpegOut); } if (err.ffmpegErr) { console.log('---metadata stderr---'); console.log(err.ffmpegErr); } if (err.spawnErr) { console.log('---metadata spawn error---'); console.log(err.spawnErr.stack || err.spawnErr); } if (stdout) { console.log('---stdout---'); console.log(stdout); } if (stderr) { console.log('---stderr---'); console.log(stderr); } } }, logOutput: function(stdout, stderr) { if (stdout) { console.log('---stdout---'); console.log(stdout); } if (stderr) { console.log('---stderr---'); console.log(stderr); } } }; ================================================ FILE: test/metadata.test.js ================================================ /*jshint node:true*/ /*global describe,it,before*/ 'use strict'; var Ffmpeg = require('../index'), path = require('path'), fs = require('fs'), Readable = require('stream').Readable, assert = require('assert'), exec = require('child_process').exec, testhelper = require('./helpers'); describe('Metadata', function() { before(function(done) { // check for ffmpeg installation this.testfile = path.join(__dirname, 'assets', 'testvideo-43.avi'); var self = this; exec(testhelper.getFfmpegCheck(), function(err) { if (!err) { // check if file exists fs.exists(self.testfile, function(exists) { if (exists) { done(); } else { done(new Error('test video file does not exist, check path (' + self.testfile + ')')); } }); } else { done(new Error('cannot run test without ffmpeg installed, aborting test...')); } }); }); it('should provide an ffprobe entry point', function(done) { (typeof Ffmpeg.ffprobe).should.equal('function'); done(); }); it('should return ffprobe data as an object', function(done) { Ffmpeg.ffprobe(this.testfile, function(err, data) { testhelper.logError(err); assert.ok(!err); (typeof data).should.equal('object'); done(); }); }); it('should provide ffprobe format information', function(done) { Ffmpeg.ffprobe(this.testfile, function(err, data) { testhelper.logError(err); assert.ok(!err); ('format' in data).should.equal(true); (typeof data.format).should.equal('object'); Number(data.format.duration).should.equal(2); data.format.format_name.should.equal('avi'); done(); }); }); it('should provide ffprobe stream information', function(done) { Ffmpeg.ffprobe(this.testfile, function(err, data) { testhelper.logError(err); assert.ok(!err); ('streams' in data).should.equal(true); Array.isArray(data.streams).should.equal(true); data.streams.length.should.equal(1); data.streams[0].codec_type.should.equal('video'); data.streams[0].codec_name.should.equal('mpeg4'); Number(data.streams[0].width).should.equal(1024); done(); }); }); it('should provide ffprobe stream information with units', function(done) { Ffmpeg.ffprobe(this.testfile, ['-unit'], function(err, data) { testhelper.logError(err); assert.ok(!err); ('streams' in data).should.equal(true); Array.isArray(data.streams).should.equal(true); data.streams.length.should.equal(1); data.streams[0].bit_rate.should.equal('322427 bit/s'); done(); }); }); it('should return ffprobe errors', function(done) { Ffmpeg.ffprobe('/path/to/missing/file', function(err) { assert.ok(!!err); done(); }); }); it('should enable calling ffprobe on a command with an input file', function(done) { new Ffmpeg({ source: this.testfile }) .ffprobe(function(err, data) { testhelper.logError(err); assert.ok(!err); (typeof data).should.equal('object'); ('format' in data).should.equal(true); (typeof data.format).should.equal('object'); ('streams' in data).should.equal(true); Array.isArray(data.streams).should.equal(true); done(); }); }); it('should fail calling ffprobe on a command without input', function(done) { new Ffmpeg().ffprobe(function(err) { assert.ok(!!err); err.message.should.match(/No input specified/); done(); }); }); it('should allow calling ffprobe on stream input', function(done) { var stream = fs.createReadStream(this.testfile); new Ffmpeg() .addInput(stream) .ffprobe(function(err, data) { assert.ok(!err); data.streams.length.should.equal(1); data.format.filename.should.equal('pipe:0'); done(); }); }); }); ================================================ FILE: test/processor.test.js ================================================ /*jshint node:true*/ /*global describe,it,before,after,beforeEach,afterEach*/ 'use strict'; var FfmpegCommand = require('../index'), path = require('path'), fs = require('fs'), assert = require('assert'), os = require('os').platform(), exec = require('child_process').exec, spawn = require('child_process').spawn, async = require('async'), stream = require('stream'), testhelper = require('./helpers'); var testHTTP = 'http://127.0.0.1:8090/test.mpg'; var testRTSP = 'rtsp://127.0.0.1:5540/test-rtp.mpg'; var testRTPOut = 'rtp://127.0.0.1:5540/input.mpg'; /***************************************************************** IMPORTANT NOTE ABOUT PROCESSOR TESTS To ensure tests run reliably, you should do the following: * Any input file you use must be tested for existence before running the tests. Use the 'prerequisite' function below and add any new file there. * FfmpegCommands should be created using 'this.getCommand(args)' in the test definition, not using 'new Ffmpegcommand(args)'. This enables ensuring the command is finished before starting the next test. * Any file your test is expected to create should have their full path pushed to the 'this.files' array in the test definition, and your test should *not* remove them on completion. The cleanup hook will check all those files for existence and remove them. * Same thing with directories in the 'this.dirs' array. * If you use intervals or timeouts, please ensure they have been canceled (for intervals) or called (for timeouts) before calling the test 'done()' callback. Not abiding by those rules is BAD. You have been warned :) *****************************************************************/ describe('Processor', function() { // check prerequisites once before all tests before(function prerequisites(done) { // check for ffmpeg installation this.testdir = path.join(__dirname, 'assets'); this.testfileName = 'testvideo-43.avi'; this.testfile = path.join(this.testdir, this.testfileName); this.testfilewide = path.join(this.testdir, 'testvideo-169.avi'); this.testfilebig = path.join(this.testdir, 'testvideo-5m.mpg'); this.testfilespecial = path.join(this.testdir, 'te[s]t_ video \' _ .flv'); this.testfileaudio1 = path.join(this.testdir, 'testaudio-one.wav'); this.testfileaudio2 = path.join(this.testdir, 'testaudio-two.wav'); this.testfileaudio3 = path.join(this.testdir, 'testaudio-three.wav'); var self = this; exec(testhelper.getFfmpegCheck(), function(err) { if (!err) { // check if all test files exist async.each([ self.testfile, self.testfilewide, self.testfilebig, self.testfilespecial, self.testfileaudio1, self.testfileaudio2, self.testfileaudio3 ], function(file, cb) { fs.exists(file, function(exists) { cb(exists ? null : new Error('test video file does not exist, check path (' + file + ')')); }); }, done ); } else { done(new Error('cannot run test without ffmpeg installed, aborting test...')); } }); }); // cleanup helpers before and after all tests beforeEach(function setup(done) { var processes = this.processes = []; var outputs = this.outputs = []; // Tests should call this so that created processes are watched // for exit and checked during test cleanup this.getCommand = function(args) { var cmd = new FfmpegCommand(args); cmd.on('start', function() { processes.push(cmd.ffmpegProc); // Remove process when it exits cmd.ffmpegProc.on('exit', function() { processes.splice(processes.indexOf(cmd.ffmpegProc), 1); }); }); return cmd; }; // Tests should call this to display stdout/stderr in case of error this.saveOutput = function(stdout, stderr) { outputs.unshift([stdout, stderr]); }; this.files = []; this.dirs = []; done(); }); afterEach(function cleanup(done) { var self = this; async.series([ // Ensure every process has finished function(cb) { if (self.processes.length) { if (self.outputs.length) { testhelper.logOutput(self.outputs[0][0], self.outputs[0][1]); } self.test.error(new Error(self.processes.length + ' processes still running after "' + self.currentTest.title + '"')); cb(); } else { cb(); } }, // Ensure all created files are removed function(cb) { async.each(self.files, function(file, cb) { fs.exists(file, function(exists) { if (exists) { fs.unlink(file, cb); } else { if (self.outputs.length) { testhelper.logOutput(self.outputs[0][0], self.outputs[0][1]); } self.test.error(new Error('Expected created file ' + file + ' by "' + self.currentTest.title + '"')); cb(); } }); }, cb); }, // Ensure all created dirs are removed function(cb) { async.each(self.dirs, function(dir, cb) { fs.exists(dir, function(exists) { if (exists) { fs.rmdir(dir, cb); } else { if (self.outputs.length) { testhelper.logOutput(self.outputs[0][0], self.outputs[0][1]); } self.test.error(new Error('Expected created directory ' + dir + ' by "' + self.currentTest.title + '"')); cb(); } }); }, cb); } ], done ); }); describe('Process controls', function() { // Skip all niceness tests on windows var skipNiceness = os.match(/win(32|64)/); var skipRenice = false; (skipNiceness ? it.skip : it)('should properly limit niceness', function() { this.getCommand({ source: this.testfile, logger: testhelper.logger, timeout: 0.02 }) .renice(100).options.niceness.should.equal(20); }); ((skipNiceness || skipRenice) ? it.skip : it)('should dynamically renice process', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testProcessRenice.avi'); this.files.push(testFile); var ffmpegJob = this.getCommand({ source: this.testfilebig, logger: testhelper.logger, timeout: 2 }) .usingPreset('divx'); var startCalled = false; var reniced = false; ffmpegJob .on('start', function() { startCalled = true; setTimeout(function() { ffmpegJob.renice(5); setTimeout(function() { exec('ps -p ' + ffmpegJob.ffmpegProc.pid + ' -o ni=', function(err, stdout) { assert.ok(!err); parseInt(stdout, 10).should.equal(5); reniced = true; }); }, 500); }, 500); ffmpegJob.ffmpegProc.on('exit', function() { reniced.should.equal(true); done(); }); }) .on('error', function() { reniced.should.equal(true); startCalled.should.equal(true); }) .on('end', function() { console.log('end was called, expected a timeout'); assert.ok(false); done(); }) .saveToFile(testFile); }); it('should change the working directory', function(done) { var testFile = path.join(this.testdir, 'testvideo.avi'); this.files.push(testFile); this.getCommand({ source: this.testfileName, logger: testhelper.logger, cwd: this.testdir }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .saveToFile(testFile); }); it('should kill the process on timeout', function(done) { var testFile = path.join(__dirname, 'assets', 'testProcessKillTimeout.avi'); this.files.push(testFile); var command = this.getCommand({ source: this.testfilebig, logger: testhelper.logger, timeout: 1}); var self = this; command .usingPreset('divx') .on('start', function() { command.ffmpegProc.on('exit', function() { done(); }); }) .on('error', function(err, stdout, stderr) { self.saveOutput(stdout, stderr); err.message.indexOf('timeout').should.not.equal(-1); }) .on('end', function() { console.log('end was called, expected a timeout'); assert.ok(false); done(); }) .saveToFile(testFile); }); it('should not keep node process running on completion', function(done) { var script = ` var ffmpeg = require('.'); ffmpeg('${this.testfilebig}', { timeout: 60 }) .addOption('-t', 1) .addOption('-f', 'null') .saveToFile('/dev/null'); `; exec(`node -e "${script}"`, { timeout: 1000 }, done); }); it('should kill the process with .kill', function(done) { var testFile = path.join(__dirname, 'assets', 'testProcessKill.avi'); this.files.push(testFile); var ffmpegJob = this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .usingPreset('divx'); var startCalled = false; var errorCalled = false; ffmpegJob .on('start', function() { startCalled = true; setTimeout(function() { ffmpegJob.kill(); }, 500); ffmpegJob.ffmpegProc.on('exit', function() { setTimeout(function() { errorCalled.should.equal(true); done(); }, 1000); }); }) .on('error', function(err) { err.message.indexOf('ffmpeg was killed with signal SIGKILL').should.not.equal(-1); startCalled.should.equal(true); errorCalled = true; }) .on('end', function() { console.log('end was called, expected an error'); assert.ok(false); done(); }) .saveToFile(testFile); }); it('should send the process custom signals with .kill(signal)', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testProcessKillCustom.avi'); this.files.push(testFile); var ffmpegJob = this.getCommand({ source: this.testfilebig, logger: testhelper.logger, timeout: 2 }) .usingPreset('divx'); var startCalled = true; var errorCalled = false; ffmpegJob .on('start', function() { startCalled = true; setTimeout(function() { ffmpegJob.kill('SIGSTOP'); }, 500); ffmpegJob.ffmpegProc.on('exit', function() { errorCalled.should.equal(true); done(); }); }) .on('error', function(err) { startCalled.should.equal(true); err.message.indexOf('timeout').should.not.equal(-1); errorCalled = true; ffmpegJob.kill('SIGCONT'); }) .on('end', function() { console.log('end was called, expected a timeout'); assert.ok(false); done(); }) .saveToFile(testFile); }); }); describe('Events', function() { it('should report codec data through \'codecData\' event', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testOnCodecData.avi'); this.files.push(testFile); this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .on('codecData', function(data) { data.should.have.property('audio'); data.should.have.property('video'); }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .saveToFile(testFile); }); it('should report codec data through \'codecData\' event on piped inputs', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testOnCodecData.avi') this.files.push(testFile); this.getCommand({ source: fs.createReadStream(this.testfilebig), logger: testhelper.logger }) .on('codecData', function(data) { data.should.have.property('audio'); data.should.have.property('video'); }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .saveToFile(testFile); }); it('should report codec data through \'codecData\' for multiple inputs', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testOnCodecData.wav') this.files.push(testFile); this.getCommand({ logger: testhelper.logger }) .input(this.testfileaudio1) .input(this.testfileaudio2) .on('codecData', function(data1, data2) { data1.should.have.property('audio'); data2.should.have.property('audio'); }) .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .mergeToFile(testFile); }); it('should report progress through \'progress\' event', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testOnProgress.avi'); var gotProgress = false; this.files.push(testFile); this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .on('progress', function() { gotProgress = true; }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { gotProgress.should.equal(true); done(); }) .saveToFile(testFile); }); it('should report start of ffmpeg process through \'start\' event', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testStart.avi'); var startCalled = false; this.files.push(testFile); this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .on('start', function(cmdline) { startCalled = true; // Only test a subset of command line cmdline.indexOf('ffmpeg').should.equal(0); cmdline.indexOf('testvideo-5m').should.not.equal(-1); cmdline.indexOf('-b:a 128k').should.not.equal(-1); }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { startCalled.should.equal(true); done(); }) .saveToFile(testFile); }); it('should report output lines through \'stderr\' event', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testStderr.avi'); var lines = []; this.files.push(testFile); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .on('stderr', function(line) { lines.push(line); }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { lines.length.should.above(0); lines[0].should.startWith('ffmpeg version'); lines.filter(function(l) { return l.indexOf('Press [q]') === 0. }).length.should.above(0); done(); }) .saveToFile(testFile); }); }); describe('Output limiting', function() { it('should limit stdout/stderr lines', function(done) { this.timeout(60000); var testFile = path.join(__dirname, 'assets', 'testLimit10.avi'); this.files.push(testFile); this.getCommand({ stdoutLines: 10, source: this.testfile, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function(stdout, stderr) { stdout.split('\n').length.should.below(11); stderr.split('\n').length.should.below(11); done(); }) .saveToFile(testFile); }); }); describe('takeScreenshots', function() { function testScreenshots(title, name, config, files) { it(title, function(done) { var filenamesCalled = false; var testFolder = path.join(__dirname, 'assets', 'screenshots_' + name); var context = this; files.forEach(function(file) { context.files.push(path.join(testFolder, file)); }); this.dirs.push(testFolder); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('filenames', function(filenames) { filenamesCalled = true; filenames.length.should.equal(files.length); filenames.forEach(function(file, index) { file.should.equal(files[index]); }); }) .on('end', function() { filenamesCalled.should.equal(true); fs.readdir(testFolder, function(err, content) { var tnCount = 0; content.forEach(function(file) { if (file.indexOf('.png') > -1) { tnCount++; } }); tnCount.should.equal(files.length); files.forEach(function(file) { content.indexOf(file).should.not.equal(-1); }); done(); }); }) .takeScreenshots(config, testFolder); }); } testScreenshots( 'should take screenshots from a list of number timemarks', 'timemarks_num', { timemarks: [ 0.5, 1 ] }, ['tn_1.png', 'tn_2.png'] ); testScreenshots( 'should take screenshots from a list of string timemarks', 'timemarks_string', { timemarks: [ '0.5', '1' ] }, ['tn_1.png', 'tn_2.png'] ); testScreenshots( 'should take screenshots from a list of string timemarks', 'timemarks_hms', { timemarks: [ '00:00:00.500', '00:01' ] }, ['tn_1.png', 'tn_2.png'] ); testScreenshots( 'should support "timestamps" instead of "timemarks"', 'timestamps', { timestamps: [ 0.5, 1 ] }, ['tn_1.png', 'tn_2.png'] ); testScreenshots( 'should replace %i with the screenshot index', 'filename_i', { timemarks: [ 0.5, 1 ], filename: 'shot_%i.png' }, ['shot_1.png', 'shot_2.png'] ); testScreenshots( 'should replace %000i with the padded screenshot index', 'filename_0i', { timemarks: [ 0.5, 1 ], filename: 'shot_%000i.png' }, ['shot_0001.png', 'shot_0002.png'] ); testScreenshots( 'should replace %s with the screenshot timestamp', 'filename_s', { timemarks: [ 0.5, '40%', 1 ], filename: 'shot_%s.png' }, ['shot_0.5.png', 'shot_0.8.png', 'shot_1.png'] ); testScreenshots( 'should replace %f with the input filename', 'filename_f', { timemarks: [ 0.5, 1 ], filename: 'shot_%f_%i.png' }, ['shot_testvideo-43.avi_1.png', 'shot_testvideo-43.avi_2.png'] ); testScreenshots( 'should replace %b with the input basename', 'filename_b', { timemarks: [ 0.5, 1 ], filename: 'shot_%b_%i.png' }, ['shot_testvideo-43_1.png', 'shot_testvideo-43_2.png'] ); testScreenshots( 'should replace %r with the output resolution', 'filename_r', { timemarks: [ 0.5, 1 ], filename: 'shot_%r_%i.png' }, ['shot_1024x768_1.png', 'shot_1024x768_2.png'] ); testScreenshots( 'should replace %w and %h with the output resolution', 'filename_wh', { timemarks: [ 0.5, 1 ], filename: 'shot_%wx%h_%i.png' }, ['shot_1024x768_1.png', 'shot_1024x768_2.png'] ); testScreenshots( 'should automatically add %i when no variable replacement is present', 'filename_add_i', { timemarks: [ 0.5, 1 ], filename: 'shot_%b.png' }, ['shot_testvideo-43_1.png', 'shot_testvideo-43_2.png'] ); testScreenshots( 'should automatically compute timestamps from the "count" option', 'count', { count: 3, filename: 'shot_%s.png' }, ['shot_0.5.png', 'shot_1.png', 'shot_1.5.png'] ); testScreenshots( 'should enable setting screenshot size', 'size', { count: 3, filename: 'shot_%r.png', size: '150x?' }, ['shot_150x112_1.png', 'shot_150x112_2.png', 'shot_150x112_3.png'] ); testScreenshots( 'a single screenshot should not have a _1 file name suffix', 'no_suffix', { timemarks: [ 0.5 ] }, ['tn.png'] ); }); describe('saveToFile', function() { it('should save the output file properly to disk', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertToFile.avi'); this.files.push(testFile); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); it('should save an output file with special characters properly to disk', function(done) { var testFile = path.join(__dirname, 'assets', 'te[s]t video \' " .avi'); this.files.push(testFile); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .saveToFile(testFile); }); it('should save output files with special characters', function(done) { var testFile = path.join(__dirname, 'assets', '[test "special \' char*cters \n.avi'); this.files.push(testFile); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); it('should accept a stream as its source', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertFromStreamToFile.avi'); this.files.push(testFile); var instream = fs.createReadStream(this.testfile); this.getCommand({ source: instream, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); it('should pass input stream errors through to error handler', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertFromStream.avi') const readError = new Error('Read Error') const instream = new (require('stream').Readable)({ read() { process.nextTick(() => this.emit('error', readError)) } }) const command = this.getCommand({ source: instream, logger: testhelper.logger }) let startCalled = false const self = this command .usingPreset('divx') .on('start', function() { startCalled = true command.ffmpegProc.on('exit', function() { fs.exists(testFile, (exists) => { exists.should.be.false() done() }) }) }) .on('error', function(err, stdout, stderr) { self.saveOutput(stdout, stderr) startCalled.should.be.true() assert.ok(err) err.message.indexOf('Input stream error: ').should.equal(0) assert.strictEqual(err.inputStreamError, readError) }) .on('end', function(stdout, stderr) { testhelper.logOutput(stdout, stderr) console.log('end was called, expected a error') assert.ok(false) done() }) .saveToFile(testFile) }) }); describe('mergeToFile', function() { it('should merge multiple files', function(done) { var testFile = path.join(__dirname, 'assets', 'testMergeAddOption.wav'); this.files.push(testFile); this.getCommand({source: this.testfileaudio1, logger: testhelper.logger}) .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .mergeAdd(this.testfileaudio2) .mergeAdd(this.testfileaudio3) .mergeToFile(testFile); }); }); describe('writeToStream', function() { it('should save the output file properly to disk using a stream', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertToStream.avi'); this.files.push(testFile); var outstream = fs.createWriteStream(testFile); this.getCommand({ source: this.testfile, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function(stdout, stderr) { fs.exists(testFile, function(exist) { if (!exist) { console.log(stderr); } exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .writeToStream(outstream, {end:true}); }); it('should accept a stream as its source', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertFromStreamToStream.avi'); this.files.push(testFile); var instream = fs.createReadStream(this.testfile); var outstream = fs.createWriteStream(testFile); this.getCommand({ source: instream, logger: testhelper.logger }) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function(stdout,stderr) { fs.exists(testFile, function(exist) { if (!exist) { console.log(stderr); } exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .writeToStream(outstream); }); (process.version.match(/v0\.8\./) ? it.skip : it)('should return a PassThrough stream when called with no arguments on node >=0.10', function(done) { var testFile = path.join(__dirname, 'assets', 'testConvertToStream.avi'); this.files.push(testFile); var outstream = fs.createWriteStream(testFile); var command = this.getCommand({ source: this.testfile, logger: testhelper.logger }); command .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function(stdout, stderr) { fs.exists(testFile, function(exist) { if (!exist) { console.log(stderr); } exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }); var passthrough = command.writeToStream({end: true}); passthrough.should.instanceof(stream.PassThrough); passthrough.pipe(outstream); }); (process.version.match(/v0\.8\./) ? it : it.skip)('should throw an error when called with no arguments on node 0.8', function() { (function() { new FfmpegCommand().writeToStream({end: true}); }).should.throw(/PassThrough stream is not supported on node v0.8/); }); it('should pass output stream errors through to error handler', function(done) { const writeError = new Error('Write Error') const outstream = new (require('stream').Writable)({ write(chunk, encoding, callback) { callback(writeError) } }) const command = this.getCommand({ source: this.testfile, logger: testhelper.logger }) let startCalled = false const self = this command .usingPreset('divx') .on('start', function() { startCalled = true command.ffmpegProc.on('exit', function() { done() }) }) .on('error', function(err, stdout, stderr) { self.saveOutput(stdout, stderr) startCalled.should.be.true() assert.ok(err) err.message.indexOf('Output stream error: ').should.equal(0) assert.strictEqual(err.outputStreamError, writeError) }) .on('end', function(stdout, stderr) { console.log('end was called, expected a error') testhelper.logOutput(stdout, stderr) assert.ok(false) done() }) .writeToStream(outstream) }) }); describe('Outputs', function() { it('should create multiple outputs', function(done) { this.timeout(30000); var testFile1 = path.join(__dirname, 'assets', 'testMultipleOutput1.avi'); this.files.push(testFile1); var testFile2 = path.join(__dirname, 'assets', 'testMultipleOutput2.avi'); this.files.push(testFile2); var testFile3 = path.join(__dirname, 'assets', 'testMultipleOutput3.mp4'); this.files.push(testFile3); this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .output(testFile1) .withAudioCodec('vorbis') .withVideoCodec('copy') .output(testFile2) .withAudioCodec('libmp3lame') .withVideoCodec('copy') .output(testFile3) .withSize('160x120') .withAudioCodec('aac') .withVideoCodec('libx264') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { async.map( [testFile1, testFile2, testFile3], function(file, cb) { fs.exists(file, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(file, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); cb(err); }); }); }, function(err) { testhelper.logError(err); assert.ok(!err); done(); } ); }) .run(); }); }); describe('Inputs', function() { it('should take input from a file with special characters', function(done) { var testFile = path.join(__dirname, 'assets', 'testSpecialInput.avi'); this.files.push(testFile); this.getCommand({ source: this.testfilespecial, logger: testhelper.logger, timeout: 10 }) .takeFrames(50) .usingPreset('divx') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); }); describe.skip('Remote I/O', function() { this.timeout(60000); var ffserver; before(function(done) { testhelper.logger.debug('spawning ffserver'); ffserver = spawn( 'ffserver', ['-d','-f', path.join(__dirname, 'assets', 'ffserver.conf')], { cwd: path.join(__dirname, 'assets') } ); // Wait for ffserver to be ready var isready = false; function ready() { if (!isready) { testhelper.logger.debug('ffserver is ready'); isready = true; done(); } } ffserver.stdout.on('data', function(d) { if (d.toString().match(/server started/i)) { ready(); } }); ffserver.stderr.on('data', function(d) { if (d.toString().match(/server started/i)) { ready(); } }); }); beforeEach(function(done) { setTimeout(done, 5000); }); after(function(done) { ffserver.kill(); setTimeout(done, 1000); }); it('should take input from a RTSP stream', function(done) { var testFile = path.join(__dirname, 'assets', 'testRTSPInput.avi'); this.files.push(testFile); this.getCommand({ source: encodeURI(testRTSP), logger: testhelper.logger, timeout: 0 }) .takeFrames(10) .usingPreset('divx') .withSize('320x240') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); it('should take input from an URL', function(done) { var testFile = path.join(__dirname, 'assets', 'testURLInput.avi'); this.files.push(testFile); this.getCommand({ source: testHTTP, logger: testhelper.logger, timeout: 0 }) .takeFrames(5) .usingPreset('divx') .withSize('320x240') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { fs.exists(testFile, function(exist) { exist.should.equal(true); // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { assert.ok(!err && stats); stats.size.should.above(0); stats.isFile().should.equal(true); done(); }); }); }) .saveToFile(testFile); }); it('should output to a RTP stream', function(done) { this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .videoCodec('libx264') .audioCodec('copy') .on('error', function(err, stdout, stderr) { testhelper.logError(err, stdout, stderr); assert.ok(!err); }) .on('end', function() { done(); }) .save(testRTPOut); }); }); describe('Errors', function() { it('should report an error when ffmpeg has been killed', function(done) { this.timeout(10000); var testFile = path.join(__dirname, 'assets', 'testErrorKill.avi'); this.files.push(testFile); var command = this.getCommand({ source: this.testfilebig, logger: testhelper.logger }); command .usingPreset('divx') .on('start', function() { setTimeout(function() { command.kill('SIGKILL'); }, 1000); }) .on('error', function(err) { err.message.should.match(/ffmpeg was killed with signal SIGKILL/); done(); }) .on('end', function() { assert.ok(false); }) .saveToFile(testFile); }); it('should report ffmpeg errors', function(done) { this.getCommand({ source: this.testfilebig, logger: testhelper.logger }) .addOption('-invalidoption') .on('error', function(err) { setTimeout(done, 1000); err.message.should.match(/Unrecognized option 'invalidoption'/); }) .saveToFile('/will/not/be/created/anyway'); }); }); }); ================================================ FILE: test/utils.test.js ================================================ /*jshint node:true*/ /*global describe,it*/ 'use strict'; var utils = require('../lib/utils'); describe('Utilities', function() { describe('Argument list helper', function() { it('Should add arguments to the list', function() { var args = utils.args(); args('-one'); args('-two', 'two-param'); args('-three', 'three-param1', 'three-param2'); args(['-four', 'four-param', '-five', '-five-param']); args.get().length.should.equal(10); }); it('Should return the argument list', function() { var args = utils.args(); args('-one'); args('-two', 'two-param'); args('-three', 'three-param1', 'three-param2'); args(['-four', 'four-param', '-five', '-five-param']); var arr = args.get(); Array.isArray(arr).should.equal(true); arr.length.should.equal(10); arr.indexOf('-three').should.equal(3); arr.indexOf('four-param').should.equal(7); }); it('Should clear the argument list', function() { var args = utils.args(); args('-one'); args('-two', 'two-param'); args('-three', 'three-param1', 'three-param2'); args(['-four', 'four-param', '-five', '-five-param']); args.clear(); args.get().length.should.equal(0); }); it('Should retrieve arguments from the list', function() { var args = utils.args(); args('-one'); args('-two', 'two-param'); args('-three', 'three-param1', 'three-param2'); args(['-four', 'four-param', '-five', '-five-param']); var one = args.find('-one'); Array.isArray(one).should.equal(true); one.length.should.equal(0); var two = args.find('-two', 1); Array.isArray(two).should.equal(true); two.length.should.equal(1); two[0].should.equal('two-param'); var three = args.find('-three', 2); Array.isArray(three).should.equal(true); three.length.should.equal(2); three[0].should.equal('three-param1'); three[1].should.equal('three-param2'); var nope = args.find('-nope', 2); (typeof nope).should.equal('undefined'); }); it('Should remove arguments from the list', function() { var args = utils.args(); args('-one'); args('-two', 'two-param'); args('-three', 'three-param1', 'three-param2'); args(['-four', 'four-param', '-five', '-five-param']); args.remove('-four', 1); var arr = args.get(); arr.length.should.equal(8); arr[5].should.equal('three-param2'); arr[6].should.equal('-five'); args.remove('-one'); arr = args.get(); arr.length.should.equal(7); arr[0].should.equal('-two'); args.remove('-three', 2); arr = args.get(); arr.length.should.equal(4); arr[1].should.equal('two-param'); arr[2].should.equal('-five'); }); }); describe('timemarkToSeconds', function() { it('should correctly convert a simple timestamp', function() { utils.timemarkToSeconds('00:02:00.00').should.be.equal(120); }); it('should correctly convert a complex timestamp', function() { utils.timemarkToSeconds('00:08:09.10').should.be.equal(489.1); }); it('should correclty convert a simple float string timestamp', function() { utils.timemarkToSeconds('132.44').should.be.equal(132.44); }); it('should correclty convert a simple float timestamp', function() { utils.timemarkToSeconds(132.44).should.be.equal(132.44); }); }); describe('Lines ring buffer', function() { it('should append lines', function() { var ring = utils.linesRing(100); ring.append('foo\nbar\nbaz\n'); ring.append('foo\nbar\nbaz\n'); ring.get().should.equal('foo\nbar\nbaz\nfoo\nbar\nbaz\n'); }); it('should append partial lines', function() { var ring = utils.linesRing(100); ring.append('foo'); ring.append('bar\nbaz'); ring.append('moo'); ring.get().should.equal('foobar\nbazmoo'); }); it('should call line callbacks', function() { var lines = []; function cb(l) { lines.push(l); } var lines2 = []; function cb2(l) { lines2.push(l); } var ring = utils.linesRing(100); ring.callback(cb); ring.callback(cb2); ring.append('foo\nbar\nbaz'); lines.length.should.equal(2); lines[0].should.equal('foo'); lines[1].should.equal('bar'); lines2.length.should.equal(2); lines2[0].should.equal('foo'); lines2[1].should.equal('bar'); ring.append('moo\nmeow\n'); lines.length.should.equal(4); lines[2].should.equal('bazmoo'); lines[3].should.equal('meow'); lines2.length.should.equal(4); lines2[2].should.equal('bazmoo'); lines2[3].should.equal('meow'); }); it('should close correctly', function() { var lines = []; function cb(l) { lines.push(l); } var ring = utils.linesRing(100); ring.callback(cb); ring.append('foo\nbar\nbaz'); lines.length.should.equal(2); lines[0].should.equal('foo'); lines[1].should.equal('bar'); ring.close(); lines.length.should.equal(3); lines[2].should.equal('baz'); ring.append('moo\nmeow\n'); lines.length.should.equal(3); ring.get().should.equal('foo\nbar\nbaz'); }); it('should limit lines', function() { var ring = utils.linesRing(2); ring.append('foo\nbar\nbaz'); ring.get().should.equal('bar\nbaz'); ring.append('foo\nbar'); ring.get().should.equal('bazfoo\nbar'); }); it('should allow unlimited lines', function() { var ring = utils.linesRing(0); ring.append('foo\nbar\nbaz'); ring.get().should.equal('foo\nbar\nbaz'); ring.append('foo\nbar'); ring.get().should.equal('foo\nbar\nbazfoo\nbar'); }); }); }); ================================================ FILE: tools/jsdoc-aliases.js ================================================ /*jshint node:true*/ 'use strict'; function createAlias(doclet, alias) { var clone = {}; Object.keys(doclet).forEach(function(key) { clone[key] = doclet[key]; }); if (alias.indexOf('#') !== -1) { clone.longname = alias; clone.memberof = alias.split('#')[0]; clone.name = alias.split('#')[1]; } else { clone.longname = clone.memberof + '#' + alias; clone.name = alias; } delete clone.returns; delete clone.examples; delete clone.meta; delete clone.aliases; clone.isAlias = true; clone.description = 'Alias for ' + doclet.longname + ''; return clone; } exports.handlers = { parseComplete: function(e) { var doclets = e.doclets.slice(); doclets.forEach(function(doclet) { // Duplicate doclets with aliases if (doclet.aliases) { doclet.aliases.forEach(function(alias) { e.doclets.push(createAlias(doclet, alias)); }); } }); } }; exports.defineTags = function(dict) { dict.defineTag('aliases', { onTagged: function(doclet, tag) { doclet.aliases = tag.text.split(','); } }); dict.defineTag('category', { onTagged: function(doclet, tag) { doclet.category = tag.text; } }); }; ================================================ FILE: tools/jsdoc-conf.json ================================================ { "opts": { "recurse": true, "verbose": true, "destination": "doc", "template": "tools/jsdoc-template" }, "source": { "include": ["README.md", "lib"], "excludePattern": "lib\/presets" }, "plugins": [ "plugins/markdown", "tools/jsdoc-aliases.js" ], "markdown": { "parser": "evilstreak", "dialect": "Markuru" } } ================================================ FILE: tools/jsdoc-template/README.md ================================================ The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://documentcloud.github.com/underscore/#template). Updated for node-fluent-ffmpeg to handle @aliases and @category tags. ================================================ FILE: tools/jsdoc-template/publish.js ================================================ /*global env: true */ 'use strict'; var template = require('jsdoc/template'), fs = require('jsdoc/fs'), path = require('jsdoc/path'), taffy = require('taffydb').taffy, logger = require('jsdoc/util/logger'), helper = require('jsdoc/util/templateHelper'), htmlsafe = helper.htmlsafe, linkto = helper.linkto, resolveAuthorLinks = helper.resolveAuthorLinks, scopeToPunc = helper.scopeToPunc, hasOwnProp = Object.prototype.hasOwnProperty, data, view, outdir = env.opts.destination; function find(spec) { return helper.find(data, spec); } function tutoriallink(tutorial) { return helper.toTutorial(tutorial, null, { tag: 'em', classname: 'disabled', prefix: 'Tutorial: ' }); } function getAncestorLinks(doclet) { return helper.getAncestorLinks(data, doclet); } function getCategoryLink(className, cat) { return '' + cat + ' methods'; } function hashToLink(doclet, hash) { if ( !/^(#.+)/.test(hash) ) { return hash; } var url = helper.createLink(doclet); url = url.replace(/(#.+|$)/, hash); return '' + hash + ''; } function needsSignature(doclet) { var needsSig = false; // function and class definitions always get a signature if (doclet.kind === 'function' || doclet.kind === 'class') { needsSig = true; } // typedefs that contain functions get a signature, too else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names && doclet.type.names.length) { for (var i = 0, l = doclet.type.names.length; i < l; i++) { if (doclet.type.names[i].toLowerCase() === 'function') { needsSig = true; break; } } } return needsSig; } function addSignatureParams(f) { var params = helper.getSignatureParams(f, 'optional'); f.signature = (f.signature || '') + '('+params.join(', ')+')'; } function addSignatureReturns(f) { var returnTypes = helper.getSignatureReturns(f); f.signature = '' + (f.signature || '') + '' + '' + (returnTypes && returnTypes.length ? ' → {' + returnTypes.join('|') + '}' : '') + ''; } function addSignatureTypes(f) { var types = helper.getSignatureTypes(f); f.signature = (f.signature || '') + ''+(types.length? ' :'+types.join('|') : '')+''; } function addAttribs(f) { var attribs = helper.getAttribs(f); f.attribs = '' + htmlsafe(attribs.length ? // we want the template output to say 'abstract', not 'virtual' '<' + attribs.join(', ').replace('virtual', 'abstract') + '> ' : '') + ''; } function shortenPaths(files, commonPrefix) { Object.keys(files).forEach(function(file) { files[file].shortened = files[file].resolved.replace(commonPrefix, '') // always use forward slashes .replace(/\\/g, '/'); }); return files; } function getPathFromDoclet(doclet) { if (!doclet.meta) { return; } return doclet.meta.path && doclet.meta.path !== 'null' ? path.join(doclet.meta.path, doclet.meta.filename) : doclet.meta.filename; } function generate(title, docs, filename, resolveLinks) { resolveLinks = resolveLinks === false ? false : true; var docData = { title: title, docs: docs }; var outpath = path.join(outdir, filename), html = view.render('container.tmpl', docData); if (resolveLinks) { html = helper.resolveLinks(html); // turn {@link foo} into foo } // Ensure
 tags have pretty print class
    html = html.replace(/
/g, '
');

    fs.writeFileSync(outpath, html, 'utf8');
}

function generateSourceFiles(sourceFiles, encoding) {
    encoding = encoding || 'utf8';
    Object.keys(sourceFiles).forEach(function(file) {
        var source;
        // links are keyed to the shortened path in each doclet's `meta.shortpath` property
        var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
        helper.registerLink(sourceFiles[file].shortened, sourceOutfile);

        try {
            source = {
                kind: 'source',
                code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
            };
        }
        catch(e) {
            logger.error('Error while generating source file %s: %s', file, e.message);
        }

        generate('Source: ' + sourceFiles[file].shortened, [source], sourceOutfile,
            false);
    });
}

/**
 * Look for classes or functions with the same name as modules (which indicates that the module
 * exports only that class or function), then attach the classes or functions to the `module`
 * property of the appropriate module doclets. The name of each class or function is also updated
 * for display purposes. This function mutates the original arrays.
 *
 * @private
 * @param {Array.} doclets - The array of classes and functions to
 * check.
 * @param {Array.} modules - The array of module doclets to search.
 */
function attachModuleSymbols(doclets, modules) {
    var symbols = {};

    // build a lookup table
    doclets.forEach(function(symbol) {
        symbols[symbol.longname] = symbol;
    });

    return modules.map(function(module) {
        if (symbols[module.longname]) {
            module.module = symbols[module.longname];
            module.module.name = module.module.name.replace('module:', 'require("') + '")';
        }
    });
}

function buildReadmeNav(readme) {
    var nav = '';

    var prevLevel = '0';
    nav += '
    '; readme = readme.replace(/([^<]*)<\/h[23]>/g, function(match, level, title) { if (title.trim().length > 0) { var titlelink = title.toLowerCase().replace(/[^a-z]/g, '-'); if (level === '2') { if (prevLevel === '2' || prevLevel === '3') { nav += '
'; } nav += '
  • ' + title + '
    • '; } else { nav += '
    • ' + title + '
    • '; } prevLevel = level; match = '' + match; } return match; }); nav += '
    '; return { nav: nav, readme: readme }; } /** * Create the navigation sidebar. * @param {String} readmeNav The readme TOC * @param {object} members The members that will be used to create the sidebar. * @param {array} members.classes * @param {array} members.externals * @param {array} members.globals * @param {array} members.mixins * @param {array} members.modules * @param {array} members.namespaces * @param {array} members.tutorials * @param {array} members.events * @return {string} The HTML for the navigation sidebar. */ function buildNav(readmeNav, members) { var nav = '

    Index

    ' + readmeNav, seen = {}, hasClassList = false, classNav = '', globalNav = ''; if (members.modules.length) { nav += '

    Modules

      '; members.modules.forEach(function(m) { if ( !hasOwnProp.call(seen, m.longname) ) { nav += '
    • '+linkto(m.longname, m.name)+'
    • '; } seen[m.longname] = true; }); nav += '
    '; } if (members.externals.length) { nav += '

    Externals

      '; members.externals.forEach(function(e) { if ( !hasOwnProp.call(seen, e.longname) ) { nav += '
    • '+linkto( e.longname, e.name.replace(/(^"|"$)/g, '') )+'
    • '; } seen[e.longname] = true; }); nav += '
    '; } if (members.classes.length) { members.classes.forEach(function(c) { if ( !hasOwnProp.call(seen, c.longname) ) { classNav += '
  • '+linkto(c.longname, c.name)+'
  • '; if (c.longname in members.categories) { classNav += '
      ' + members.categories[c.longname].reduce(function(nav, cat) { return nav + '
    • ' + getCategoryLink(c.longname, cat) + '
    • '; }, '') + '
    '; } } seen[c.longname] = true; }); if (classNav !== '') { nav += '

    Classes

      '; nav += classNav; nav += '
    '; } } /*if (members.events.length) { nav += '

    Events

      '; members.events.forEach(function(e) { if ( !hasOwnProp.call(seen, e.longname) ) { nav += '
    • '+linkto(e.longname, e.name)+'
    • '; } seen[e.longname] = true; }); nav += '
    '; }*/ if (members.namespaces.length) { nav += '

    Namespaces

      '; members.namespaces.forEach(function(n) { if ( !hasOwnProp.call(seen, n.longname) ) { nav += '
    • '+linkto(n.longname, n.name)+'
    • '; } seen[n.longname] = true; }); nav += '
    '; } if (members.mixins.length) { nav += '

    Mixins

      '; members.mixins.forEach(function(m) { if ( !hasOwnProp.call(seen, m.longname) ) { nav += '
    • '+linkto(m.longname, m.name)+'
    • '; } seen[m.longname] = true; }); nav += '
    '; } if (members.tutorials.length) { nav += '

    Tutorials

      '; members.tutorials.forEach(function(t) { nav += '
    • '+tutoriallink(t.name)+'
    • '; }); nav += '
    '; } if (members.globals.length) { members.globals.forEach(function(g) { if ( g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname) ) { globalNav += '
  • ' + linkto(g.longname, g.name) + '
  • '; } seen[g.longname] = true; }); if (!globalNav) { // turn the heading into a link so you can actually get to the global page nav += '

    ' + linkto('global', 'Global') + '

    '; } else { nav += '

    Global

      ' + globalNav + '
    '; } } return nav; } /** @param {TAFFY} taffyData See . @param {object} opts @param {Tutorial} tutorials */ exports.publish = function(taffyData, opts, tutorials) { data = taffyData; var conf = env.conf.templates || {}; conf['default'] = conf['default'] || {}; var templatePath = opts.template; view = new template.Template(templatePath + '/tmpl'); // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness // doesn't try to hand them out later var indexUrl = helper.getUniqueFilename('index'); // don't call registerLink() on this one! 'index' is also a valid longname var globalUrl = helper.getUniqueFilename('global'); helper.registerLink('global', globalUrl); // set up templating view.layout = conf['default'].layoutFile ? path.getResourcePath(path.dirname(conf['default'].layoutFile), path.basename(conf['default'].layoutFile) ) : 'layout.tmpl'; // set up tutorials for helper helper.setTutorials(tutorials); data = helper.prune(data); data.sort('longname, version, since'); helper.addEventListeners(data); var sourceFiles = {}; var sourceFilePaths = []; data().each(function(doclet) { doclet.attribs = ''; if (doclet.examples) { doclet.examples = doclet.examples.map(function(example) { var caption, code; if (example.match(/^\s*([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) { caption = RegExp.$1; code = RegExp.$3; } return { caption: caption || '', code: code || example }; }); } if (doclet.see) { doclet.see.forEach(function(seeItem, i) { doclet.see[i] = hashToLink(doclet, seeItem); }); } // build a list of source files var sourcePath; if (doclet.meta) { sourcePath = getPathFromDoclet(doclet); sourceFiles[sourcePath] = { resolved: sourcePath, shortened: null }; if (sourceFilePaths.indexOf(sourcePath) === -1) { sourceFilePaths.push(sourcePath); } } }); // update outdir if necessary, then create outdir var packageInfo = ( find({kind: 'package'}) || [] ) [0]; if (packageInfo && packageInfo.name) { outdir = path.join(outdir, packageInfo.name, packageInfo.version); } fs.mkPath(outdir); // copy the template's static files to outdir var fromDir = path.join(templatePath, 'static'); var staticFiles = fs.ls(fromDir, 3); staticFiles.forEach(function(fileName) { var toDir = fs.toDir( fileName.replace(fromDir, outdir) ); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); // copy user-specified static files to outdir var staticFilePaths; var staticFileFilter; var staticFileScanner; if (conf['default'].staticFiles) { staticFilePaths = conf['default'].staticFiles.paths || []; staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf['default'].staticFiles); staticFileScanner = new (require('jsdoc/src/scanner')).Scanner(); staticFilePaths.forEach(function(filePath) { var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); extraStaticFiles.forEach(function(fileName) { var sourcePath = fs.toDir(filePath); var toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); }); } if (sourceFilePaths.length) { sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) ); } data().each(function(doclet) { var url = helper.createLink(doclet); helper.registerLink(doclet.longname, url); // add a shortened version of the full path var docletPath; if (doclet.meta) { docletPath = getPathFromDoclet(doclet); docletPath = sourceFiles[docletPath].shortened; if (docletPath) { doclet.meta.shortpath = docletPath; } } }); data().each(function(doclet) { var url = helper.longnameToUrl[doclet.longname]; if (url.indexOf('#') > -1) { doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); } else { doclet.id = doclet.name; } if ( needsSignature(doclet) ) { addSignatureParams(doclet); addSignatureReturns(doclet); addAttribs(doclet); } }); // do this after the urls have all been generated data().each(function(doclet) { doclet.ancestors = getAncestorLinks(doclet); if (doclet.kind === 'member') { addSignatureTypes(doclet); addAttribs(doclet); } if (doclet.kind === 'constant') { addSignatureTypes(doclet); addAttribs(doclet); doclet.kind = 'member'; } }); var members = helper.getMembers(data); members.tutorials = tutorials.children; members.categories = data('method').get().reduce(function(cats, method) { if (!(method.memberof in cats)) { cats[method.memberof] = []; } var cat = method.category || 'Other'; if (cats[method.memberof].indexOf(cat) === -1) { cats[method.memberof].push(cat); cats[method.memberof] = cats[method.memberof].sort(); } return cats; }, {}); // output pretty-printed source files by default var outputSourceFiles = conf['default'] && conf['default'].outputSourceFiles !== false ? true : false; // add template helpers view.find = find; view.linkto = linkto; view.resolveAuthorLinks = resolveAuthorLinks; view.tutoriallink = tutoriallink; view.htmlsafe = htmlsafe; view.outputSourceFiles = outputSourceFiles; // Build readme nav var readmeNav = buildReadmeNav(opts.readme); opts.readme = readmeNav.readme; // once for all view.nav = buildNav(readmeNav.nav, members); attachModuleSymbols( find({ kind: ['class', 'function'], longname: {left: 'module:'} }), members.modules ); // generate the pretty-printed source files first so other pages can link to them if (outputSourceFiles) { generateSourceFiles(sourceFiles, opts.encoding); } if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); } // index page displays information from package.json and lists files var files = find({kind: 'file'}), packages = find({kind: 'package'}); generate('Index', packages.concat( [{kind: 'mainpage', readme: opts.readme, longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'}] ).concat(files), indexUrl); // set up the lists that we'll use to generate pages var classes = taffy(members.classes); var modules = taffy(members.modules); var namespaces = taffy(members.namespaces); var mixins = taffy(members.mixins); var externals = taffy(members.externals); Object.keys(helper.longnameToUrl).forEach(function(longname) { var myClasses = helper.find(classes, {longname: longname}); if (myClasses.length) { generate('Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]); } var myModules = helper.find(modules, {longname: longname}); if (myModules.length) { generate('Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]); } var myNamespaces = helper.find(namespaces, {longname: longname}); if (myNamespaces.length) { generate('Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]); } var myMixins = helper.find(mixins, {longname: longname}); if (myMixins.length) { generate('Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]); } var myExternals = helper.find(externals, {longname: longname}); if (myExternals.length) { generate('External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname]); } }); // TODO: move the tutorial functions to templateHelper.js function generateTutorial(title, tutorial, filename) { var tutorialData = { title: title, header: tutorial.title, content: tutorial.parse(), children: tutorial.children }; var tutorialPath = path.join(outdir, filename), html = view.render('tutorial.tmpl', tutorialData); // yes, you can use {@link} in tutorials too! html = helper.resolveLinks(html); // turn {@link foo} into foo fs.writeFileSync(tutorialPath, html, 'utf8'); } // tutorials can have only one parent so there is no risk for loops function saveChildren(node) { node.children.forEach(function(child) { generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); saveChildren(child); }); } saveChildren(tutorials); }; ================================================ FILE: tools/jsdoc-template/static/scripts/linenumber.js ================================================ /*global document */ (function() { var source = document.getElementsByClassName('prettyprint source linenums'); var i = 0; var lineNumber = 0; var lineId; var lines; var totalLines; var anchorHash; if (source && source[0]) { anchorHash = document.location.hash.substring(1); lines = source[0].getElementsByTagName('li'); totalLines = lines.length; for (; i < totalLines; i++) { lineNumber++; lineId = 'line' + lineNumber; lines[i].id = lineId; if (lineId === anchorHash) { lines[i].className += ' selected'; } } } })(); ================================================ FILE: tools/jsdoc-template/static/scripts/prettify/Apache-License-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: tools/jsdoc-template/static/scripts/prettify/lang-css.js ================================================ PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); ================================================ FILE: tools/jsdoc-template/static/scripts/prettify/prettify.js ================================================ var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p h2 { margin-top: 6px; } h3 { font-size: 150%; font-weight: bold; letter-spacing: -0.01em; margin-top: 16px; margin: 50px 0 3px 0; } h4 { font-size: 130%; font-weight: bold; letter-spacing: -0.01em; margin-top: 16px; margin: 18px 0 3px 0; color: #526492; } h5, .container-overview .subsection-title { font-size: 120%; font-weight: bold; letter-spacing: -0.01em; margin: 8px 0 3px -16px; } h6 { font-size: 100%; letter-spacing: -0.01em; margin: 6px 0 3px 0; font-style: italic; } article > dl, article > pre { margin-left: 2em; } .ancestors { color: #999; } .ancestors a { color: #999 !important; text-decoration: none; } .important { font-weight: bold; color: #950B02; } .yes-def { text-indent: -1000px; } .type-signature { color: #aaa; } .name, .signature { font-family: Consolas, "Lucida Console", Monaco, monospace; } .details { margin-top: 14px; border-left: 2px solid #DDD; } .details dt { width:100px; float:left; padding-left: 10px; padding-top: 6px; } .details dd { margin-left: 50px; } .details ul { margin: 0; } .details ul { list-style-type: none; } .details li { margin-left: 30px; padding-top: 6px; } .details pre.prettyprint { margin: 0 } .details .object-value { padding-top: 0; } .description { margin-bottom: 1em; margin-left: -16px; margin-top: 1em; } .code-caption { font-style: italic; font-family: Palatino, 'Palatino Linotype', serif; font-size: 107%; margin: 0; } .prettyprint { border: 1px solid #ddd; width: 80%; overflow: auto; } .prettyprint.source { width: inherit; } .prettyprint code { font-family: Consolas, 'Lucida Console', Monaco, monospace; font-size: 100%; line-height: 18px; display: block; padding: 4px 12px; margin: 0; background-color: #fff; color: #000; } .prettyprint code span.line { display: inline-block; } .prettyprint.linenums { padding-left: 70px; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .prettyprint.linenums ol { padding-left: 0; } .prettyprint.linenums li { border-left: 3px #ddd solid; } .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { background-color: lightyellow; } .prettyprint.linenums li * { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } .params, .props { border-spacing: 0; border: 0; border-collapse: collapse; } .params .name, .props .name, .name code { color: #526492; font-family: Consolas, 'Lucida Console', Monaco, monospace; font-size: 100%; } .params td, .params th, .props td, .props th { border: 1px solid #ddd; margin: 0px; text-align: left; vertical-align: top; padding: 4px 6px; display: table-cell; } .params thead tr, .props thead tr { background-color: #ddd; font-weight: bold; } .params .params thead tr, .props .props thead tr { background-color: #fff; font-weight: bold; } .params th, .props th { border-right: 1px solid #aaa; } .params thead .last, .props thead .last { border-right: 1px solid #ddd; } .params td.description > p:first-child { margin-top: 0; padding-top: 0; } .params td.description > p:last-child { margin-bottom: 0; padding-bottom: 0; } .disabled { color: #454545; } ================================================ FILE: tools/jsdoc-template/static/styles/prettify-jsdoc.css ================================================ /* JSDoc prettify.js theme */ /* plain text */ .pln { color: #000000; font-weight: normal; font-style: normal; } /* string content */ .str { color: #006400; font-weight: normal; font-style: normal; } /* a keyword */ .kwd { color: #000000; font-weight: bold; font-style: normal; } /* a comment */ .com { font-weight: normal; font-style: italic; } /* a type name */ .typ { color: #000000; font-weight: normal; font-style: normal; } /* a literal value */ .lit { color: #006400; font-weight: normal; font-style: normal; } /* punctuation */ .pun { color: #000000; font-weight: bold; font-style: normal; } /* lisp open bracket */ .opn { color: #000000; font-weight: bold; font-style: normal; } /* lisp close bracket */ .clo { color: #000000; font-weight: bold; font-style: normal; } /* a markup tag name */ .tag { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute name */ .atn { color: #006400; font-weight: normal; font-style: normal; } /* a markup attribute value */ .atv { color: #006400; font-weight: normal; font-style: normal; } /* a declaration */ .dec { color: #000000; font-weight: bold; font-style: normal; } /* a variable name */ .var { color: #000000; font-weight: normal; font-style: normal; } /* a function name */ .fun { color: #000000; font-weight: bold; font-style: normal; } /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } ================================================ FILE: tools/jsdoc-template/static/styles/prettify-tomorrow.css ================================================ /* Tomorrow Theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* Pretty printing styles. Used with prettify.js. */ /* SPAN elements with the classes below are added by prettyprint. */ /* plain text */ .pln { color: #4d4d4c; } @media screen { /* string content */ .str { color: #718c00; } /* a keyword */ .kwd { color: #8959a8; } /* a comment */ .com { color: #8e908c; } /* a type name */ .typ { color: #4271ae; } /* a literal value */ .lit { color: #f5871f; } /* punctuation */ .pun { color: #4d4d4c; } /* lisp open bracket */ .opn { color: #4d4d4c; } /* lisp close bracket */ .clo { color: #4d4d4c; } /* a markup tag name */ .tag { color: #c82829; } /* a markup attribute name */ .atn { color: #f5871f; } /* a markup attribute value */ .atv { color: #3e999f; } /* a declaration */ .dec { color: #f5871f; } /* a variable name */ .var { color: #c82829; } /* a function name */ .fun { color: #4271ae; } } /* Use higher contrast and text-weight for printable form. */ @media print, projection { .str { color: #060; } .kwd { color: #006; font-weight: bold; } .com { color: #600; font-style: italic; } .typ { color: #404; font-weight: bold; } .lit { color: #044; } .pun, .opn, .clo { color: #440; } .tag { color: #006; font-weight: bold; } .atn { color: #404; } .atv { color: #060; } } /* Style */ /* pre.prettyprint { background: white; font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; line-height: 1.5; border: 1px solid #ccc; padding: 10px; } */ /* Specify class=linenums on a pre to get line numbering */ ol.linenums { margin-top: 0; margin-bottom: 0; } /* IE indents via margin-left */ li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9 { /* */ } /* Alternate shading for lines */ li.L1, li.L3, li.L5, li.L7, li.L9 { /* */ } ================================================ FILE: tools/jsdoc-template/tmpl/aliases.tmpl ================================================ 1) { ?>
    ================================================ FILE: tools/jsdoc-template/tmpl/container.tmpl ================================================

    Example 1? 's':'' ?>

    Extends

    Mixes In

    Requires

    Classes

    Namespaces

    Members

    Type Definitions

    Events

    ================================================ FILE: tools/jsdoc-template/tmpl/details.tmpl ================================================ " + data.defaultvalue + ""; defaultObjectClass = ' class="object-value"'; } ?>
    Properties:
    Version:
    Since:
    Inherited From:
    Deprecated:
    • Yes
      Author:
      License:
      Default Value:
        >
      Source:
      • ,
      Tutorials:
      See:
      To Do:
      ================================================ FILE: tools/jsdoc-template/tmpl/example.tmpl ================================================
      ================================================ FILE: tools/jsdoc-template/tmpl/examples.tmpl ================================================

      ================================================ FILE: tools/jsdoc-template/tmpl/exceptions.tmpl ================================================
      Type
      ================================================ FILE: tools/jsdoc-template/tmpl/layout.tmpl ================================================ JSDoc: <?js= title ?>


      Documentation generated by JSDoc on
      ================================================ FILE: tools/jsdoc-template/tmpl/mainpage.tmpl ================================================

      ================================================ FILE: tools/jsdoc-template/tmpl/members.tmpl ================================================

      Type:
      Fires:
      Example 1? 's':'' ?>
      ================================================ FILE: tools/jsdoc-template/tmpl/method.tmpl ================================================

      Type:
      This:
      Parameters:
      Requires:
      Fires:
      Listens to Events:
      Listeners of This Event:
      Throws:
      1) { ?>
      Returns:
      1) { ?>
      Example 1? 's':'' ?>:
      Alias 1 ? 'es' : '' ?>:
      ================================================ FILE: tools/jsdoc-template/tmpl/params.tmpl ================================================
      Name Type Argument Default Description
      <optional>
      <nullable>
      <repeatable>
      Properties
      ================================================ FILE: tools/jsdoc-template/tmpl/properties.tmpl ================================================
      Name Type Argument Default Description
      <optional>
      <nullable>
      Properties
      ================================================ FILE: tools/jsdoc-template/tmpl/returns.tmpl ================================================
      Type
      ================================================ FILE: tools/jsdoc-template/tmpl/source.tmpl ================================================
      ================================================ FILE: tools/jsdoc-template/tmpl/tutorial.tmpl ================================================
      0) { ?>

      ================================================ FILE: tools/jsdoc-template/tmpl/type.tmpl ================================================ |