utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. /*jshint node:true*/
  2. 'use strict';
  3. var exec = require('child_process').exec;
  4. var isWindows = require('os').platform().match(/win(32|64)/);
  5. var which = require('which');
  6. var nlRegexp = /\r\n|\r|\n/g;
  7. var streamRegexp = /^\[?(.*?)\]?$/;
  8. var filterEscapeRegexp = /[,]/;
  9. var whichCache = {};
  10. /**
  11. * Parse progress line from ffmpeg stderr
  12. *
  13. * @param {String} line progress line
  14. * @return progress object
  15. * @private
  16. */
  17. function parseProgressLine(line) {
  18. var progress = {};
  19. // Remove all spaces after = and trim
  20. line = line.replace(/=\s+/g, '=').trim();
  21. var progressParts = line.split(' ');
  22. // Split every progress part by "=" to get key and value
  23. for(var i = 0; i < progressParts.length; i++) {
  24. var progressSplit = progressParts[i].split('=', 2);
  25. var key = progressSplit[0];
  26. var value = progressSplit[1];
  27. // This is not a progress line
  28. if(typeof value === 'undefined')
  29. return null;
  30. progress[key] = value;
  31. }
  32. return progress;
  33. }
  34. var utils = module.exports = {
  35. isWindows: isWindows,
  36. streamRegexp: streamRegexp,
  37. /**
  38. * Copy an object keys into another one
  39. *
  40. * @param {Object} source source object
  41. * @param {Object} dest destination object
  42. * @private
  43. */
  44. copy: function(source, dest) {
  45. Object.keys(source).forEach(function(key) {
  46. dest[key] = source[key];
  47. });
  48. },
  49. /**
  50. * Create an argument list
  51. *
  52. * Returns a function that adds new arguments to the list.
  53. * It also has the following methods:
  54. * - clear() empties the argument list
  55. * - get() returns the argument list
  56. * - find(arg, count) finds 'arg' in the list and return the following 'count' items, or undefined if not found
  57. * - remove(arg, count) remove 'arg' in the list as well as the following 'count' items
  58. *
  59. * @private
  60. */
  61. args: function() {
  62. var list = [];
  63. // Append argument(s) to the list
  64. var argfunc = function() {
  65. if (arguments.length === 1 && Array.isArray(arguments[0])) {
  66. list = list.concat(arguments[0]);
  67. } else {
  68. list = list.concat([].slice.call(arguments));
  69. }
  70. };
  71. // Clear argument list
  72. argfunc.clear = function() {
  73. list = [];
  74. };
  75. // Return argument list
  76. argfunc.get = function() {
  77. return list;
  78. };
  79. // Find argument 'arg' in list, and if found, return an array of the 'count' items that follow it
  80. argfunc.find = function(arg, count) {
  81. var index = list.indexOf(arg);
  82. if (index !== -1) {
  83. return list.slice(index + 1, index + 1 + (count || 0));
  84. }
  85. };
  86. // Find argument 'arg' in list, and if found, remove it as well as the 'count' items that follow it
  87. argfunc.remove = function(arg, count) {
  88. var index = list.indexOf(arg);
  89. if (index !== -1) {
  90. list.splice(index, (count || 0) + 1);
  91. }
  92. };
  93. // Clone argument list
  94. argfunc.clone = function() {
  95. var cloned = utils.args();
  96. cloned(list);
  97. return cloned;
  98. };
  99. return argfunc;
  100. },
  101. /**
  102. * Generate filter strings
  103. *
  104. * @param {String[]|Object[]} filters filter specifications. When using objects,
  105. * each must have the following properties:
  106. * @param {String} filters.filter filter name
  107. * @param {String|Array} [filters.inputs] (array of) input stream specifier(s) for the filter,
  108. * defaults to ffmpeg automatically choosing the first unused matching streams
  109. * @param {String|Array} [filters.outputs] (array of) output stream specifier(s) for the filter,
  110. * defaults to ffmpeg automatically assigning the output to the output file
  111. * @param {Object|String|Array} [filters.options] filter options, can be omitted to not set any options
  112. * @return String[]
  113. * @private
  114. */
  115. makeFilterStrings: function(filters) {
  116. return filters.map(function(filterSpec) {
  117. if (typeof filterSpec === 'string') {
  118. return filterSpec;
  119. }
  120. var filterString = '';
  121. // Filter string format is:
  122. // [input1][input2]...filter[output1][output2]...
  123. // The 'filter' part can optionaly have arguments:
  124. // filter=arg1:arg2:arg3
  125. // filter=arg1=v1:arg2=v2:arg3=v3
  126. // Add inputs
  127. if (Array.isArray(filterSpec.inputs)) {
  128. filterString += filterSpec.inputs.map(function(streamSpec) {
  129. return streamSpec.replace(streamRegexp, '[$1]');
  130. }).join('');
  131. } else if (typeof filterSpec.inputs === 'string') {
  132. filterString += filterSpec.inputs.replace(streamRegexp, '[$1]');
  133. }
  134. // Add filter
  135. filterString += filterSpec.filter;
  136. // Add options
  137. if (filterSpec.options) {
  138. if (typeof filterSpec.options === 'string' || typeof filterSpec.options === 'number') {
  139. // Option string
  140. filterString += '=' + filterSpec.options;
  141. } else if (Array.isArray(filterSpec.options)) {
  142. // Option array (unnamed options)
  143. filterString += '=' + filterSpec.options.map(function(option) {
  144. if (typeof option === 'string' && option.match(filterEscapeRegexp)) {
  145. return '\'' + option + '\'';
  146. } else {
  147. return option;
  148. }
  149. }).join(':');
  150. } else if (Object.keys(filterSpec.options).length) {
  151. // Option object (named options)
  152. filterString += '=' + Object.keys(filterSpec.options).map(function(option) {
  153. var value = filterSpec.options[option];
  154. if (typeof value === 'string' && value.match(filterEscapeRegexp)) {
  155. value = '\'' + value + '\'';
  156. }
  157. return option + '=' + value;
  158. }).join(':');
  159. }
  160. }
  161. // Add outputs
  162. if (Array.isArray(filterSpec.outputs)) {
  163. filterString += filterSpec.outputs.map(function(streamSpec) {
  164. return streamSpec.replace(streamRegexp, '[$1]');
  165. }).join('');
  166. } else if (typeof filterSpec.outputs === 'string') {
  167. filterString += filterSpec.outputs.replace(streamRegexp, '[$1]');
  168. }
  169. return filterString;
  170. });
  171. },
  172. /**
  173. * Search for an executable
  174. *
  175. * Uses 'which' or 'where' depending on platform
  176. *
  177. * @param {String} name executable name
  178. * @param {Function} callback callback with signature (err, path)
  179. * @private
  180. */
  181. which: function(name, callback) {
  182. if (name in whichCache) {
  183. return callback(null, whichCache[name]);
  184. }
  185. which(name, function(err, result){
  186. if (err) {
  187. // Treat errors as not found
  188. return callback(null, whichCache[name] = '');
  189. }
  190. callback(null, whichCache[name] = result);
  191. });
  192. },
  193. /**
  194. * Convert a [[hh:]mm:]ss[.xxx] timemark into seconds
  195. *
  196. * @param {String} timemark timemark string
  197. * @return Number
  198. * @private
  199. */
  200. timemarkToSeconds: function(timemark) {
  201. if (typeof timemark === 'number') {
  202. return timemark;
  203. }
  204. if (timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) {
  205. return Number(timemark);
  206. }
  207. var parts = timemark.split(':');
  208. // add seconds
  209. var secs = Number(parts.pop());
  210. if (parts.length) {
  211. // add minutes
  212. secs += Number(parts.pop()) * 60;
  213. }
  214. if (parts.length) {
  215. // add hours
  216. secs += Number(parts.pop()) * 3600;
  217. }
  218. return secs;
  219. },
  220. /**
  221. * Extract codec data from ffmpeg stderr and emit 'codecData' event if appropriate
  222. * Call it with an initially empty codec object once with each line of stderr output until it returns true
  223. *
  224. * @param {FfmpegCommand} command event emitter
  225. * @param {String} stderrLine ffmpeg stderr output line
  226. * @param {Object} codecObject object used to accumulate codec data between calls
  227. * @return {Boolean} true if codec data is complete (and event was emitted), false otherwise
  228. * @private
  229. */
  230. extractCodecData: function(command, stderrLine, codecsObject) {
  231. var inputPattern = /Input #[0-9]+, ([^ ]+),/;
  232. var durPattern = /Duration\: ([^,]+)/;
  233. var audioPattern = /Audio\: (.*)/;
  234. var videoPattern = /Video\: (.*)/;
  235. if (!('inputStack' in codecsObject)) {
  236. codecsObject.inputStack = [];
  237. codecsObject.inputIndex = -1;
  238. codecsObject.inInput = false;
  239. }
  240. var inputStack = codecsObject.inputStack;
  241. var inputIndex = codecsObject.inputIndex;
  242. var inInput = codecsObject.inInput;
  243. var format, dur, audio, video;
  244. if (format = stderrLine.match(inputPattern)) {
  245. inInput = codecsObject.inInput = true;
  246. inputIndex = codecsObject.inputIndex = codecsObject.inputIndex + 1;
  247. inputStack[inputIndex] = { format: format[1], audio: '', video: '', duration: '' };
  248. } else if (inInput && (dur = stderrLine.match(durPattern))) {
  249. inputStack[inputIndex].duration = dur[1];
  250. } else if (inInput && (audio = stderrLine.match(audioPattern))) {
  251. audio = audio[1].split(', ');
  252. inputStack[inputIndex].audio = audio[0];
  253. inputStack[inputIndex].audio_details = audio;
  254. } else if (inInput && (video = stderrLine.match(videoPattern))) {
  255. video = video[1].split(', ');
  256. inputStack[inputIndex].video = video[0];
  257. inputStack[inputIndex].video_details = video;
  258. } else if (/Output #\d+/.test(stderrLine)) {
  259. inInput = codecsObject.inInput = false;
  260. } else if (/Stream mapping:|Press (\[q\]|ctrl-c) to stop/.test(stderrLine)) {
  261. command.emit.apply(command, ['codecData'].concat(inputStack));
  262. return true;
  263. }
  264. return false;
  265. },
  266. /**
  267. * Extract progress data from ffmpeg stderr and emit 'progress' event if appropriate
  268. *
  269. * @param {FfmpegCommand} command event emitter
  270. * @param {String} stderrLine ffmpeg stderr data
  271. * @private
  272. */
  273. extractProgress: function(command, stderrLine) {
  274. var progress = parseProgressLine(stderrLine);
  275. if (progress) {
  276. // build progress report object
  277. var ret = {
  278. frames: parseInt(progress.frame, 10),
  279. currentFps: parseInt(progress.fps, 10),
  280. currentKbps: progress.bitrate ? parseFloat(progress.bitrate.replace('kbits/s', '')) : 0,
  281. targetSize: parseInt(progress.size || progress.Lsize, 10),
  282. timemark: progress.time
  283. };
  284. // calculate percent progress using duration
  285. if (command._ffprobeData && command._ffprobeData.format && command._ffprobeData.format.duration) {
  286. var duration = Number(command._ffprobeData.format.duration);
  287. if (!isNaN(duration))
  288. ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100;
  289. }
  290. command.emit('progress', ret);
  291. }
  292. },
  293. /**
  294. * Extract error message(s) from ffmpeg stderr
  295. *
  296. * @param {String} stderr ffmpeg stderr data
  297. * @return {String}
  298. * @private
  299. */
  300. extractError: function(stderr) {
  301. // Only return the last stderr lines that don't start with a space or a square bracket
  302. return stderr.split(nlRegexp).reduce(function(messages, message) {
  303. if (message.charAt(0) === ' ' || message.charAt(0) === '[') {
  304. return [];
  305. } else {
  306. messages.push(message);
  307. return messages;
  308. }
  309. }, []).join('\n');
  310. },
  311. /**
  312. * Creates a line ring buffer object with the following methods:
  313. * - append(str) : appends a string or buffer
  314. * - get() : returns the whole string
  315. * - close() : prevents further append() calls and does a last call to callbacks
  316. * - callback(cb) : calls cb for each line (incl. those already in the ring)
  317. *
  318. * @param {Numebr} maxLines maximum number of lines to store (<= 0 for unlimited)
  319. */
  320. linesRing: function(maxLines) {
  321. var cbs = [];
  322. var lines = [];
  323. var current = null;
  324. var closed = false
  325. var max = maxLines - 1;
  326. function emit(line) {
  327. cbs.forEach(function(cb) { cb(line); });
  328. }
  329. return {
  330. callback: function(cb) {
  331. lines.forEach(function(l) { cb(l); });
  332. cbs.push(cb);
  333. },
  334. append: function(str) {
  335. if (closed) return;
  336. if (str instanceof Buffer) str = '' + str;
  337. if (!str || str.length === 0) return;
  338. var newLines = str.split(nlRegexp);
  339. if (newLines.length === 1) {
  340. if (current !== null) {
  341. current = current + newLines.shift();
  342. } else {
  343. current = newLines.shift();
  344. }
  345. } else {
  346. if (current !== null) {
  347. current = current + newLines.shift();
  348. emit(current);
  349. lines.push(current);
  350. }
  351. current = newLines.pop();
  352. newLines.forEach(function(l) {
  353. emit(l);
  354. lines.push(l);
  355. });
  356. if (max > -1 && lines.length > max) {
  357. lines.splice(0, lines.length - max);
  358. }
  359. }
  360. },
  361. get: function() {
  362. if (current !== null) {
  363. return lines.concat([current]).join('\n');
  364. } else {
  365. return lines.join('\n');
  366. }
  367. },
  368. close: function() {
  369. if (closed) return;
  370. if (current !== null) {
  371. emit(current);
  372. lines.push(current);
  373. if (max > -1 && lines.length > max) {
  374. lines.shift();
  375. }
  376. current = null;
  377. }
  378. closed = true;
  379. }
  380. };
  381. }
  382. };