Source: processor.js

  1. /*jshint node:true*/
  2. 'use strict';
  3. var spawn = require('child_process').spawn;
  4. var path = require('path');
  5. var fs = require('fs');
  6. var async = require('async');
  7. var utils = require('./utils');
  8. var nlRegexp = /\r\n|\r|\n/g;
  9. /*
  10. *! Processor methods
  11. */
  12. /**
  13. * Run ffprobe asynchronously and store data in command
  14. *
  15. * @param {FfmpegCommand} command
  16. * @private
  17. */
  18. function runFfprobe(command) {
  19. command.ffprobe(0, function(err, data) {
  20. command._ffprobeData = data;
  21. });
  22. }
  23. module.exports = function(proto) {
  24. /**
  25. * Emitted just after ffmpeg has been spawned.
  26. *
  27. * @event FfmpegCommand#start
  28. * @param {String} command ffmpeg command line
  29. */
  30. /**
  31. * Emitted when ffmpeg reports progress information
  32. *
  33. * @event FfmpegCommand#progress
  34. * @param {Object} progress progress object
  35. * @param {Number} progress.frames number of frames transcoded
  36. * @param {Number} progress.currentFps current processing speed in frames per second
  37. * @param {Number} progress.currentKbps current output generation speed in kilobytes per second
  38. * @param {Number} progress.targetSize current output file size
  39. * @param {String} progress.timemark current video timemark
  40. * @param {Number} [progress.percent] processing progress (may not be available depending on input)
  41. */
  42. /**
  43. * Emitted when ffmpeg outputs to stderr
  44. *
  45. * @event FfmpegCommand#stderr
  46. * @param {String} line stderr output line
  47. */
  48. /**
  49. * Emitted when ffmpeg reports input codec data
  50. *
  51. * @event FfmpegCommand#codecData
  52. * @param {Object} codecData codec data object
  53. * @param {String} codecData.format input format name
  54. * @param {String} codecData.audio input audio codec name
  55. * @param {String} codecData.audio_details input audio codec parameters
  56. * @param {String} codecData.video input video codec name
  57. * @param {String} codecData.video_details input video codec parameters
  58. */
  59. /**
  60. * Emitted when an error happens when preparing or running a command
  61. *
  62. * @event FfmpegCommand#error
  63. * @param {Error} error error object
  64. * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
  65. * @param {String|null} stderr ffmpeg stderr
  66. */
  67. /**
  68. * Emitted when a command finishes processing
  69. *
  70. * @event FfmpegCommand#end
  71. * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
  72. * @param {String|null} stderr ffmpeg stderr
  73. */
  74. /**
  75. * Spawn an ffmpeg process
  76. *
  77. * The 'options' argument may contain the following keys:
  78. * - 'niceness': specify process niceness, ignored on Windows (default: 0)
  79. * - `cwd`: change working directory
  80. * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
  81. * - 'stdoutLines': override command limit (default: use command limit)
  82. *
  83. * The 'processCB' callback, if present, is called as soon as the process is created and
  84. * receives a nodejs ChildProcess object. It may not be called at all if an error happens
  85. * before spawning the process.
  86. *
  87. * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
  88. *
  89. * @method FfmpegCommand#_spawnFfmpeg
  90. * @param {Array} args ffmpeg command line argument list
  91. * @param {Object} [options] spawn options (see above)
  92. * @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created
  93. * @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished
  94. * @private
  95. */
  96. proto._spawnFfmpeg = function(args, options, processCB, endCB) {
  97. // Enable omitting options
  98. if (typeof options === 'function') {
  99. endCB = processCB;
  100. processCB = options;
  101. options = {};
  102. }
  103. // Enable omitting processCB
  104. if (typeof endCB === 'undefined') {
  105. endCB = processCB;
  106. processCB = function() {};
  107. }
  108. var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;
  109. // Find ffmpeg
  110. this._getFfmpegPath(function(err, command) {
  111. if (err) {
  112. return endCB(err);
  113. } else if (!command || command.length === 0) {
  114. return endCB(new Error('Cannot find ffmpeg'));
  115. }
  116. // Apply niceness
  117. if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
  118. args.unshift('-n', options.niceness, command);
  119. command = 'nice';
  120. }
  121. var stdoutRing = utils.linesRing(maxLines);
  122. var stdoutClosed = false;
  123. var stderrRing = utils.linesRing(maxLines);
  124. var stderrClosed = false;
  125. // Spawn process
  126. var ffmpegProc = spawn(command, args, options);
  127. if (ffmpegProc.stderr) {
  128. ffmpegProc.stderr.setEncoding('utf8');
  129. }
  130. ffmpegProc.on('error', function(err) {
  131. endCB(err);
  132. });
  133. // Ensure we wait for captured streams to end before calling endCB
  134. var exitError = null;
  135. function handleExit(err) {
  136. if (err) {
  137. exitError = err;
  138. }
  139. if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
  140. endCB(exitError, stdoutRing, stderrRing);
  141. }
  142. }
  143. // Handle process exit
  144. var processExited = false;
  145. ffmpegProc.on('exit', function(code, signal) {
  146. processExited = true;
  147. if (signal) {
  148. handleExit(new Error('ffmpeg was killed with signal ' + signal));
  149. } else if (code) {
  150. handleExit(new Error('ffmpeg exited with code ' + code));
  151. } else {
  152. handleExit();
  153. }
  154. });
  155. // Capture stdout if specified
  156. if (options.captureStdout) {
  157. ffmpegProc.stdout.on('data', function(data) {
  158. stdoutRing.append(data);
  159. });
  160. ffmpegProc.stdout.on('close', function() {
  161. stdoutRing.close();
  162. stdoutClosed = true;
  163. handleExit();
  164. });
  165. }
  166. // Capture stderr if specified
  167. ffmpegProc.stderr.on('data', function(data) {
  168. stderrRing.append(data);
  169. });
  170. ffmpegProc.stderr.on('close', function() {
  171. stderrRing.close();
  172. stderrClosed = true;
  173. handleExit();
  174. });
  175. // Call process callback
  176. processCB(ffmpegProc, stdoutRing, stderrRing);
  177. });
  178. };
  179. /**
  180. * Build the argument list for an ffmpeg command
  181. *
  182. * @method FfmpegCommand#_getArguments
  183. * @return argument list
  184. * @private
  185. */
  186. proto._getArguments = function() {
  187. var complexFilters = this._complexFilters.get();
  188. var fileOutput = this._outputs.some(function(output) {
  189. return output.isFile;
  190. });
  191. return [].concat(
  192. // Inputs and input options
  193. this._inputs.reduce(function(args, input) {
  194. var source = (typeof input.source === 'string') ? input.source : 'pipe:0';
  195. // For each input, add input options, then '-i <source>'
  196. return args.concat(
  197. input.options.get(),
  198. ['-i', source]
  199. );
  200. }, []),
  201. // Global options
  202. this._global.get(),
  203. // Overwrite if we have file outputs
  204. fileOutput ? ['-y'] : [],
  205. // Complex filters
  206. complexFilters,
  207. // Outputs, filters and output options
  208. this._outputs.reduce(function(args, output) {
  209. var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get());
  210. var audioFilters = output.audioFilters.get();
  211. var videoFilters = output.videoFilters.get().concat(sizeFilters);
  212. var outputArg;
  213. if (!output.target) {
  214. outputArg = [];
  215. } else if (typeof output.target === 'string') {
  216. outputArg = [output.target];
  217. } else {
  218. outputArg = ['pipe:1'];
  219. }
  220. return args.concat(
  221. output.audio.get(),
  222. audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
  223. output.video.get(),
  224. videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
  225. output.options.get(),
  226. outputArg
  227. );
  228. }, [])
  229. );
  230. };
  231. /**
  232. * Prepare execution of an ffmpeg command
  233. *
  234. * Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
  235. * then builds the argument list for ffmpeg and pass them to 'callback'.
  236. *
  237. * @method FfmpegCommand#_prepare
  238. * @param {Function} callback callback with signature (err, args)
  239. * @param {Boolean} [readMetadata=false] read metadata before processing
  240. * @private
  241. */
  242. proto._prepare = function(callback, readMetadata) {
  243. var self = this;
  244. async.waterfall([
  245. // Check codecs and formats
  246. function(cb) {
  247. self._checkCapabilities(cb);
  248. },
  249. // Read metadata if required
  250. function(cb) {
  251. if (!readMetadata) {
  252. return cb();
  253. }
  254. self.ffprobe(0, function(err, data) {
  255. if (!err) {
  256. self._ffprobeData = data;
  257. }
  258. cb();
  259. });
  260. },
  261. // Check for flvtool2/flvmeta if necessary
  262. function(cb) {
  263. var flvmeta = self._outputs.some(function(output) {
  264. // Remove flvmeta flag on non-file output
  265. if (output.flags.flvmeta && !output.isFile) {
  266. self.logger.warn('Updating flv metadata is only supported for files');
  267. output.flags.flvmeta = false;
  268. }
  269. return output.flags.flvmeta;
  270. });
  271. if (flvmeta) {
  272. self._getFlvtoolPath(function(err) {
  273. cb(err);
  274. });
  275. } else {
  276. cb();
  277. }
  278. },
  279. // Build argument list
  280. function(cb) {
  281. var args;
  282. try {
  283. args = self._getArguments();
  284. } catch(e) {
  285. return cb(e);
  286. }
  287. cb(null, args);
  288. },
  289. // Add "-strict experimental" option where needed
  290. function(args, cb) {
  291. self.availableEncoders(function(err, encoders) {
  292. for (var i = 0; i < args.length; i++) {
  293. if (args[i] === '-acodec' || args[i] === '-vcodec') {
  294. i++;
  295. if ((args[i] in encoders) && encoders[args[i]].experimental) {
  296. args.splice(i + 1, 0, '-strict', 'experimental');
  297. i += 2;
  298. }
  299. }
  300. }
  301. cb(null, args);
  302. });
  303. }
  304. ], callback);
  305. if (!readMetadata) {
  306. // Read metadata as soon as 'progress' listeners are added
  307. if (this.listeners('progress').length > 0) {
  308. // Read metadata in parallel
  309. runFfprobe(this);
  310. } else {
  311. // Read metadata as soon as the first 'progress' listener is added
  312. this.once('newListener', function(event) {
  313. if (event === 'progress') {
  314. runFfprobe(this);
  315. }
  316. });
  317. }
  318. }
  319. };
  320. /**
  321. * Run ffmpeg command
  322. *
  323. * @method FfmpegCommand#run
  324. * @category Processing
  325. * @aliases exec,execute
  326. */
  327. proto.exec =
  328. proto.execute =
  329. proto.run = function() {
  330. var self = this;
  331. // Check if at least one output is present
  332. var outputPresent = this._outputs.some(function(output) {
  333. return 'target' in output;
  334. });
  335. if (!outputPresent) {
  336. throw new Error('No output specified');
  337. }
  338. // Get output stream if any
  339. var outputStream = this._outputs.filter(function(output) {
  340. return typeof output.target !== 'string';
  341. })[0];
  342. // Get input stream if any
  343. var inputStream = this._inputs.filter(function(input) {
  344. return typeof input.source !== 'string';
  345. })[0];
  346. // Ensure we send 'end' or 'error' only once
  347. var ended = false;
  348. function emitEnd(err, stdout, stderr) {
  349. if (!ended) {
  350. ended = true;
  351. if (err) {
  352. self.emit('error', err, stdout, stderr);
  353. } else {
  354. self.emit('end', stdout, stderr);
  355. }
  356. }
  357. }
  358. self._prepare(function(err, args) {
  359. if (err) {
  360. return emitEnd(err);
  361. }
  362. // Run ffmpeg
  363. self._spawnFfmpeg(
  364. args,
  365. {
  366. captureStdout: !outputStream,
  367. niceness: self.options.niceness,
  368. cwd: self.options.cwd
  369. },
  370. function processCB(ffmpegProc, stdoutRing, stderrRing) {
  371. self.ffmpegProc = ffmpegProc;
  372. self.emit('start', 'ffmpeg ' + args.join(' '));
  373. // Pipe input stream if any
  374. if (inputStream) {
  375. inputStream.source.on('error', function(err) {
  376. emitEnd(new Error('Input stream error: ' + err.message));
  377. ffmpegProc.kill();
  378. });
  379. inputStream.source.resume();
  380. inputStream.source.pipe(ffmpegProc.stdin);
  381. // Set stdin error handler on ffmpeg (prevents nodejs catching the error, but
  382. // ffmpeg will fail anyway, so no need to actually handle anything)
  383. ffmpegProc.stdin.on('error', function() {});
  384. }
  385. // Setup timeout if requested
  386. var processTimer;
  387. if (self.options.timeout) {
  388. processTimer = setTimeout(function() {
  389. var msg = 'process ran into a timeout (' + self.options.timeout + 's)';
  390. emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get());
  391. ffmpegProc.kill();
  392. }, self.options.timeout * 1000);
  393. }
  394. if (outputStream) {
  395. // Pipe ffmpeg stdout to output stream
  396. ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
  397. // Handle output stream events
  398. outputStream.target.on('close', function() {
  399. self.logger.debug('Output stream closed, scheduling kill for ffmpgeg process');
  400. // Don't kill process yet, to give a chance to ffmpeg to
  401. // terminate successfully first This is necessary because
  402. // under load, the process 'exit' event sometimes happens
  403. // after the output stream 'close' event.
  404. setTimeout(function() {
  405. emitEnd(new Error('Output stream closed'));
  406. ffmpegProc.kill();
  407. }, 20);
  408. });
  409. outputStream.target.on('error', function(err) {
  410. self.logger.debug('Output stream error, killing ffmpgeg process');
  411. emitEnd(new Error('Output stream error: ' + err.message), stdoutRing.get(), stderrRing.get());
  412. ffmpegProc.kill();
  413. });
  414. }
  415. // Setup stderr handling
  416. if (stderrRing) {
  417. // 'stderr' event
  418. if (self.listeners('stderr').length) {
  419. stderrRing.callback(function(line) {
  420. self.emit('stderr', line);
  421. });
  422. }
  423. // 'codecData' event
  424. if (self.listeners('codecData').length) {
  425. var codecDataSent = false;
  426. var codecObject = {};
  427. stderrRing.callback(function(line) {
  428. if (!codecDataSent)
  429. codecDataSent = utils.extractCodecData(self, line, codecObject);
  430. });
  431. }
  432. // 'progress' event
  433. if (self.listeners('progress').length) {
  434. var duration = 0;
  435. if (self._ffprobeData && self._ffprobeData.format && self._ffprobeData.format.duration) {
  436. duration = Number(self._ffprobeData.format.duration);
  437. }
  438. stderrRing.callback(function(line) {
  439. utils.extractProgress(self, line, duration);
  440. });
  441. }
  442. }
  443. },
  444. function endCB(err, stdoutRing, stderrRing) {
  445. delete self.ffmpegProc;
  446. if (err) {
  447. if (err.message.match(/ffmpeg exited with code/)) {
  448. // Add ffmpeg error message
  449. err.message += ': ' + utils.extractError(stderrRing.get());
  450. }
  451. emitEnd(err, stdoutRing.get(), stderrRing.get());
  452. } else {
  453. // Find out which outputs need flv metadata
  454. var flvmeta = self._outputs.filter(function(output) {
  455. return output.flags.flvmeta;
  456. });
  457. if (flvmeta.length) {
  458. self._getFlvtoolPath(function(err, flvtool) {
  459. if (err) {
  460. return emitEnd(err);
  461. }
  462. async.each(
  463. flvmeta,
  464. function(output, cb) {
  465. spawn(flvtool, ['-U', output.target])
  466. .on('error', function(err) {
  467. cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message));
  468. })
  469. .on('exit', function(code, signal) {
  470. if (code !== 0 || signal) {
  471. cb(
  472. new Error(flvtool + ' ' +
  473. (signal ? 'received signal ' + signal
  474. : 'exited with code ' + code)) +
  475. ' when running on ' + output.target
  476. );
  477. } else {
  478. cb();
  479. }
  480. });
  481. },
  482. function(err) {
  483. if (err) {
  484. emitEnd(err);
  485. } else {
  486. emitEnd(null, stdoutRing.get(), stderrRing.get());
  487. }
  488. }
  489. );
  490. });
  491. } else {
  492. emitEnd(null, stdoutRing.get(), stderrRing.get());
  493. }
  494. }
  495. }
  496. );
  497. });
  498. };
  499. /**
  500. * Renice current and/or future ffmpeg processes
  501. *
  502. * Ignored on Windows platforms.
  503. *
  504. * @method FfmpegCommand#renice
  505. * @category Processing
  506. *
  507. * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
  508. * @return FfmpegCommand
  509. */
  510. proto.renice = function(niceness) {
  511. if (!utils.isWindows) {
  512. niceness = niceness || 0;
  513. if (niceness < -20 || niceness > 20) {
  514. this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
  515. }
  516. niceness = Math.min(20, Math.max(-20, niceness));
  517. this.options.niceness = niceness;
  518. if (this.ffmpegProc) {
  519. var logger = this.logger;
  520. var pid = this.ffmpegProc.pid;
  521. var renice = spawn('renice', [niceness, '-p', pid]);
  522. renice.on('error', function(err) {
  523. logger.warn('could not renice process ' + pid + ': ' + err.message);
  524. });
  525. renice.on('exit', function(code, signal) {
  526. if (signal) {
  527. logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
  528. } else if (code) {
  529. logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
  530. } else {
  531. logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
  532. }
  533. });
  534. }
  535. }
  536. return this;
  537. };
  538. /**
  539. * Kill current ffmpeg process, if any
  540. *
  541. * @method FfmpegCommand#kill
  542. * @category Processing
  543. *
  544. * @param {String} [signal=SIGKILL] signal name
  545. * @return FfmpegCommand
  546. */
  547. proto.kill = function(signal) {
  548. if (!this.ffmpegProc) {
  549. this.logger.warn('No running ffmpeg process, cannot send signal');
  550. } else {
  551. this.ffmpegProc.kill(signal || 'SIGKILL');
  552. }
  553. return this;
  554. };
  555. };