| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="utf-8"> |
| | <title>JSDoc: Source: recipes.js</title> |
| |
|
| | <script src="scripts/prettify/prettify.js"> </script> |
| | <script src="scripts/prettify/lang-css.js"> </script> |
| | |
| | |
| | |
| | <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> |
| | <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> |
| | </head> |
| |
|
| | <body> |
| |
|
| | <div id="main"> |
| |
|
| | <h1 class="page-title">Source: recipes.js</h1> |
| |
|
| | |
| |
|
| |
|
| | |
| | <section> |
| | <article> |
| | <pre class="prettyprint source linenums"><code>/*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; |
| | }; |
| | }; |
| | </code></pre> |
| | </article> |
| | </section> |
| |
|
| |
|
| |
|
| |
|
| | </div> |
| |
|
| | <nav> |
| | <h2><a href="index.html">Index</a></h2><ul><li><a href="index.html#installation">Installation</a></li><ul></ul><li><a href="index.html#usage">Usage</a></li><ul><li><a href="index.html#prerequisites">Prerequisites</a></li><li><a href="index.html#creating-an-ffmpeg-command">Creating an FFmpeg command</a></li><li><a href="index.html#specifying-inputs">Specifying inputs</a></li><li><a href="index.html#input-options">Input options</a></li><li><a href="index.html#audio-options">Audio options</a></li><li><a href="index.html#video-options">Video options</a></li><li><a href="index.html#video-frame-size-options">Video frame size options</a></li><li><a href="index.html#specifying-multiple-outputs">Specifying multiple outputs</a></li><li><a href="index.html#output-options">Output options</a></li><li><a href="index.html#miscellaneous-options">Miscellaneous options</a></li><li><a href="index.html#setting-event-handlers">Setting event handlers</a></li><li><a href="index.html#starting-ffmpeg-processing">Starting FFmpeg processing</a></li><li><a href="index.html#controlling-the-ffmpeg-process">Controlling the FFmpeg process</a></li><li><a href="index.html#reading-video-metadata">Reading video metadata</a></li><li><a href="index.html#querying-ffmpeg-capabilities">Querying ffmpeg capabilities</a></li><li><a href="index.html#cloning-an-ffmpegcommand">Cloning an FfmpegCommand</a></li></ul><li><a href="index.html#contributing">Contributing</a></li><ul><li><a href="index.html#code-contributions">Code contributions</a></li><li><a href="index.html#documentation-contributions">Documentation contributions</a></li><li><a href="index.html#updating-the-documentation">Updating the documentation</a></li><li><a href="index.html#running-tests">Running tests</a></li></ul><li><a href="index.html#main-contributors">Main contributors</a></li><ul></ul><li><a href="index.html#license">License</a></li><ul></ul></ul><h3>Classes</h3><ul><li><a href="FfmpegCommand.html">FfmpegCommand</a></li><ul><li> <a href="FfmpegCommand.html#audio-methods">Audio methods</a></li><li> <a href="FfmpegCommand.html#capabilities-methods">Capabilities methods</a></li><li> <a href="FfmpegCommand.html#custom-options-methods">Custom options methods</a></li><li> <a href="FfmpegCommand.html#input-methods">Input methods</a></li><li> <a href="FfmpegCommand.html#metadata-methods">Metadata methods</a></li><li> <a href="FfmpegCommand.html#miscellaneous-methods">Miscellaneous methods</a></li><li> <a href="FfmpegCommand.html#other-methods">Other methods</a></li><li> <a href="FfmpegCommand.html#output-methods">Output methods</a></li><li> <a href="FfmpegCommand.html#processing-methods">Processing methods</a></li><li> <a href="FfmpegCommand.html#video-methods">Video methods</a></li><li> <a href="FfmpegCommand.html#video-size-methods">Video size methods</a></li></ul></ul> |
| | </nav> |
| |
|
| | <br clear="both"> |
| |
|
| | <footer> |
| | Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Sun May 01 2016 12:10:37 GMT+0200 (CEST) |
| | </footer> |
| |
|
| | <script> prettyPrint(); </script> |
| | <script src="scripts/linenumber.js"> </script> |
| | </body> |
| | </html> |
| |
|