recipes.js.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>JSDoc: Source: recipes.js</title>
  6. <script src="scripts/prettify/prettify.js"> </script>
  7. <script src="scripts/prettify/lang-css.js"> </script>
  8. <!--[if lt IE 9]>
  9. <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  10. <![endif]-->
  11. <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
  12. <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
  13. </head>
  14. <body>
  15. <div id="main">
  16. <h1 class="page-title">Source: recipes.js</h1>
  17. <section>
  18. <article>
  19. <pre class="prettyprint source linenums"><code>/*jshint node:true*/
  20. 'use strict';
  21. var fs = require('fs');
  22. var path = require('path');
  23. var PassThrough = require('stream').PassThrough;
  24. var async = require('async');
  25. var utils = require('./utils');
  26. /*
  27. * Useful recipes for commands
  28. */
  29. module.exports = function recipes(proto) {
  30. /**
  31. * Execute ffmpeg command and save output to a file
  32. *
  33. * @method FfmpegCommand#save
  34. * @category Processing
  35. * @aliases saveToFile
  36. *
  37. * @param {String} output file path
  38. * @return FfmpegCommand
  39. */
  40. proto.saveToFile =
  41. proto.save = function(output) {
  42. this.output(output).run();
  43. return this;
  44. };
  45. /**
  46. * Execute ffmpeg command and save output to a stream
  47. *
  48. * If 'stream' is not specified, a PassThrough stream is created and returned.
  49. * 'options' will be used when piping ffmpeg output to the output stream
  50. * (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)
  51. *
  52. * @method FfmpegCommand#pipe
  53. * @category Processing
  54. * @aliases stream,writeToStream
  55. *
  56. * @param {stream.Writable} [stream] output stream
  57. * @param {Object} [options={}] pipe options
  58. * @return Output stream
  59. */
  60. proto.writeToStream =
  61. proto.pipe =
  62. proto.stream = function(stream, options) {
  63. if (stream &amp;&amp; !('writable' in stream)) {
  64. options = stream;
  65. stream = undefined;
  66. }
  67. if (!stream) {
  68. if (process.version.match(/v0\.8\./)) {
  69. throw new Error('PassThrough stream is not supported on node v0.8');
  70. }
  71. stream = new PassThrough();
  72. }
  73. this.output(stream, options).run();
  74. return stream;
  75. };
  76. /**
  77. * Generate images from a video
  78. *
  79. * Note: this method makes the command emit a 'filenames' event with an array of
  80. * the generated image filenames.
  81. *
  82. * @method FfmpegCommand#screenshots
  83. * @category Processing
  84. * @aliases takeScreenshots,thumbnail,thumbnails,screenshot
  85. *
  86. * @param {Number|Object} [config=1] screenshot count or configuration object with
  87. * the following keys:
  88. * @param {Number} [config.count] number of screenshots to take; using this option
  89. * takes screenshots at regular intervals (eg. count=4 would take screens at 20%, 40%,
  90. * 60% and 80% of the video length).
  91. * @param {String} [config.folder='.'] output folder
  92. * @param {String} [config.filename='tn.png'] output filename pattern, may contain the following
  93. * tokens:
  94. * - '%s': offset in seconds
  95. * - '%w': screenshot width
  96. * - '%h': screenshot height
  97. * - '%r': screenshot resolution (same as '%wx%h')
  98. * - '%f': input filename
  99. * - '%b': input basename (filename w/o extension)
  100. * - '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`)
  101. * @param {Number[]|String[]} [config.timemarks] array of timemarks to take screenshots
  102. * at; each timemark may be a number of seconds, a '[[hh:]mm:]ss[.xxx]' string or a
  103. * 'XX%' string. Overrides 'count' if present.
  104. * @param {Number[]|String[]} [config.timestamps] alias for 'timemarks'
  105. * @param {Boolean} [config.fastSeek] use fast seek (less accurate)
  106. * @param {String} [config.size] screenshot size, with the same syntax as {@link FfmpegCommand#size}
  107. * @param {String} [folder] output folder (legacy alias for 'config.folder')
  108. * @return FfmpegCommand
  109. */
  110. proto.takeScreenshots =
  111. proto.thumbnail =
  112. proto.thumbnails =
  113. proto.screenshot =
  114. proto.screenshots = function(config, folder) {
  115. var self = this;
  116. var source = this._currentInput.source;
  117. config = config || { count: 1 };
  118. // Accept a number of screenshots instead of a config object
  119. if (typeof config === 'number') {
  120. config = {
  121. count: config
  122. };
  123. }
  124. // Accept a second 'folder' parameter instead of config.folder
  125. if (!('folder' in config)) {
  126. config.folder = folder || '.';
  127. }
  128. // Accept 'timestamps' instead of 'timemarks'
  129. if ('timestamps' in config) {
  130. config.timemarks = config.timestamps;
  131. }
  132. // Compute timemarks from count if not present
  133. if (!('timemarks' in config)) {
  134. if (!config.count) {
  135. throw new Error('Cannot take screenshots: neither a count nor a timemark list are specified');
  136. }
  137. var interval = 100 / (1 + config.count);
  138. config.timemarks = [];
  139. for (var i = 0; i &lt; config.count; i++) {
  140. config.timemarks.push((interval * (i + 1)) + '%');
  141. }
  142. }
  143. // Parse size option
  144. if ('size' in config) {
  145. var fixedSize = config.size.match(/^(\d+)x(\d+)$/);
  146. var fixedWidth = config.size.match(/^(\d+)x\?$/);
  147. var fixedHeight = config.size.match(/^\?x(\d+)$/);
  148. var percentSize = config.size.match(/^(\d+)%$/);
  149. if (!fixedSize &amp;&amp; !fixedWidth &amp;&amp; !fixedHeight &amp;&amp; !percentSize) {
  150. throw new Error('Invalid size parameter: ' + config.size);
  151. }
  152. }
  153. // Metadata helper
  154. var metadata;
  155. function getMetadata(cb) {
  156. if (metadata) {
  157. cb(null, metadata);
  158. } else {
  159. self.ffprobe(function(err, meta) {
  160. metadata = meta;
  161. cb(err, meta);
  162. });
  163. }
  164. }
  165. async.waterfall([
  166. // Compute percent timemarks if any
  167. function computeTimemarks(next) {
  168. if (config.timemarks.some(function(t) { return ('' + t).match(/^[\d.]+%$/); })) {
  169. if (typeof source !== 'string') {
  170. return next(new Error('Cannot compute screenshot timemarks with an input stream, please specify fixed timemarks'));
  171. }
  172. getMetadata(function(err, meta) {
  173. if (err) {
  174. next(err);
  175. } else {
  176. // Select video stream with the highest resolution
  177. var vstream = meta.streams.reduce(function(biggest, stream) {
  178. if (stream.codec_type === 'video' &amp;&amp; stream.width * stream.height > biggest.width * biggest.height) {
  179. return stream;
  180. } else {
  181. return biggest;
  182. }
  183. }, { width: 0, height: 0 });
  184. if (vstream.width === 0) {
  185. return next(new Error('No video stream in input, cannot take screenshots'));
  186. }
  187. var duration = Number(vstream.duration);
  188. if (isNaN(duration)) {
  189. duration = Number(meta.format.duration);
  190. }
  191. if (isNaN(duration)) {
  192. return next(new Error('Could not get input duration, please specify fixed timemarks'));
  193. }
  194. config.timemarks = config.timemarks.map(function(mark) {
  195. if (('' + mark).match(/^([\d.]+)%$/)) {
  196. return duration * parseFloat(mark) / 100;
  197. } else {
  198. return mark;
  199. }
  200. });
  201. next();
  202. }
  203. });
  204. } else {
  205. next();
  206. }
  207. },
  208. // Turn all timemarks into numbers and sort them
  209. function normalizeTimemarks(next) {
  210. config.timemarks = config.timemarks.map(function(mark) {
  211. return utils.timemarkToSeconds(mark);
  212. }).sort(function(a, b) { return a - b; });
  213. next();
  214. },
  215. // Add '_%i' to pattern when requesting multiple screenshots and no variable token is present
  216. function fixPattern(next) {
  217. var pattern = config.filename || 'tn.png';
  218. if (pattern.indexOf('.') === -1) {
  219. pattern += '.png';
  220. }
  221. if (config.timemarks.length > 1 &amp;&amp; !pattern.match(/%(s|0*i)/)) {
  222. var ext = path.extname(pattern);
  223. pattern = path.join(path.dirname(pattern), path.basename(pattern, ext) + '_%i' + ext);
  224. }
  225. next(null, pattern);
  226. },
  227. // Replace filename tokens (%f, %b) in pattern
  228. function replaceFilenameTokens(pattern, next) {
  229. if (pattern.match(/%[bf]/)) {
  230. if (typeof source !== 'string') {
  231. return next(new Error('Cannot replace %f or %b when using an input stream'));
  232. }
  233. pattern = pattern
  234. .replace(/%f/g, path.basename(source))
  235. .replace(/%b/g, path.basename(source, path.extname(source)));
  236. }
  237. next(null, pattern);
  238. },
  239. // Compute size if needed
  240. function getSize(pattern, next) {
  241. if (pattern.match(/%[whr]/)) {
  242. if (fixedSize) {
  243. return next(null, pattern, fixedSize[1], fixedSize[2]);
  244. }
  245. getMetadata(function(err, meta) {
  246. if (err) {
  247. return next(new Error('Could not determine video resolution to replace %w, %h or %r'));
  248. }
  249. var vstream = meta.streams.reduce(function(biggest, stream) {
  250. if (stream.codec_type === 'video' &amp;&amp; stream.width * stream.height > biggest.width * biggest.height) {
  251. return stream;
  252. } else {
  253. return biggest;
  254. }
  255. }, { width: 0, height: 0 });
  256. if (vstream.width === 0) {
  257. return next(new Error('No video stream in input, cannot replace %w, %h or %r'));
  258. }
  259. var width = vstream.width;
  260. var height = vstream.height;
  261. if (fixedWidth) {
  262. height = height * Number(fixedWidth[1]) / width;
  263. width = Number(fixedWidth[1]);
  264. } else if (fixedHeight) {
  265. width = width * Number(fixedHeight[1]) / height;
  266. height = Number(fixedHeight[1]);
  267. } else if (percentSize) {
  268. width = width * Number(percentSize[1]) / 100;
  269. height = height * Number(percentSize[1]) / 100;
  270. }
  271. next(null, pattern, Math.round(width / 2) * 2, Math.round(height / 2) * 2);
  272. });
  273. } else {
  274. next(null, pattern, -1, -1);
  275. }
  276. },
  277. // Replace size tokens (%w, %h, %r) in pattern
  278. function replaceSizeTokens(pattern, width, height, next) {
  279. pattern = pattern
  280. .replace(/%r/g, '%wx%h')
  281. .replace(/%w/g, width)
  282. .replace(/%h/g, height);
  283. next(null, pattern);
  284. },
  285. // Replace variable tokens in pattern (%s, %i) and generate filename list
  286. function replaceVariableTokens(pattern, next) {
  287. var filenames = config.timemarks.map(function(t, i) {
  288. return pattern
  289. .replace(/%s/g, utils.timemarkToSeconds(t))
  290. .replace(/%(0*)i/g, function(match, padding) {
  291. var idx = '' + (i + 1);
  292. return padding.substr(0, Math.max(0, padding.length + 1 - idx.length)) + idx;
  293. });
  294. });
  295. self.emit('filenames', filenames);
  296. next(null, filenames);
  297. },
  298. // Create output directory
  299. function createDirectory(filenames, next) {
  300. fs.exists(config.folder, function(exists) {
  301. if (!exists) {
  302. fs.mkdir(config.folder, function(err) {
  303. if (err) {
  304. next(err);
  305. } else {
  306. next(null, filenames);
  307. }
  308. });
  309. } else {
  310. next(null, filenames);
  311. }
  312. });
  313. }
  314. ], function runCommand(err, filenames) {
  315. if (err) {
  316. return self.emit('error', err);
  317. }
  318. var count = config.timemarks.length;
  319. var split;
  320. var filters = [split = {
  321. filter: 'split',
  322. options: count,
  323. outputs: []
  324. }];
  325. if ('size' in config) {
  326. // Set size to generate size filters
  327. self.size(config.size);
  328. // Get size filters and chain them with 'sizeN' stream names
  329. var sizeFilters = self._currentOutput.sizeFilters.get().map(function(f, i) {
  330. if (i > 0) {
  331. f.inputs = 'size' + (i - 1);
  332. }
  333. f.outputs = 'size' + i;
  334. return f;
  335. });
  336. // Input last size filter output into split filter
  337. split.inputs = 'size' + (sizeFilters.length - 1);
  338. // Add size filters in front of split filter
  339. filters = sizeFilters.concat(filters);
  340. // Remove size filters
  341. self._currentOutput.sizeFilters.clear();
  342. }
  343. var first = 0;
  344. for (var i = 0; i &lt; count; i++) {
  345. var stream = 'screen' + i;
  346. split.outputs.push(stream);
  347. if (i === 0) {
  348. first = config.timemarks[i];
  349. self.seekInput(first);
  350. }
  351. self.output(path.join(config.folder, filenames[i]))
  352. .frames(1)
  353. .map(stream);
  354. if (i > 0) {
  355. self.seek(config.timemarks[i] - first);
  356. }
  357. }
  358. self.complexFilter(filters);
  359. self.run();
  360. });
  361. return this;
  362. };
  363. /**
  364. * Merge (concatenate) inputs to a single file
  365. *
  366. * @method FfmpegCommand#concat
  367. * @category Processing
  368. * @aliases concatenate,mergeToFile
  369. *
  370. * @param {String|Writable} target output file or writable stream
  371. * @param {Object} [options] pipe options (only used when outputting to a writable stream)
  372. * @return FfmpegCommand
  373. */
  374. proto.mergeToFile =
  375. proto.concatenate =
  376. proto.concat = function(target, options) {
  377. // Find out which streams are present in the first non-stream input
  378. var fileInput = this._inputs.filter(function(input) {
  379. return !input.isStream;
  380. })[0];
  381. var self = this;
  382. this.ffprobe(this._inputs.indexOf(fileInput), function(err, data) {
  383. if (err) {
  384. return self.emit('error', err);
  385. }
  386. var hasAudioStreams = data.streams.some(function(stream) {
  387. return stream.codec_type === 'audio';
  388. });
  389. var hasVideoStreams = data.streams.some(function(stream) {
  390. return stream.codec_type === 'video';
  391. });
  392. // Setup concat filter and start processing
  393. self.output(target, options)
  394. .complexFilter({
  395. filter: 'concat',
  396. options: {
  397. n: self._inputs.length,
  398. v: hasVideoStreams ? 1 : 0,
  399. a: hasAudioStreams ? 1 : 0
  400. }
  401. })
  402. .run();
  403. });
  404. return this;
  405. };
  406. };
  407. </code></pre>
  408. </article>
  409. </section>
  410. </div>
  411. <nav>
  412. <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>
  413. </nav>
  414. <br clear="both">
  415. <footer>
  416. 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)
  417. </footer>
  418. <script> prettyPrint(); </script>
  419. <script src="scripts/linenumber.js"> </script>
  420. </body>
  421. </html>