videosize.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /*jshint node:true*/
  2. 'use strict';
  3. /*
  4. *! Size helpers
  5. */
  6. /**
  7. * Return filters to pad video to width*height,
  8. *
  9. * @param {Number} width output width
  10. * @param {Number} height output height
  11. * @param {Number} aspect video aspect ratio (without padding)
  12. * @param {Number} color padding color
  13. * @return scale/pad filters
  14. * @private
  15. */
  16. function getScalePadFilters(width, height, aspect, color) {
  17. /*
  18. let a be the input aspect ratio, A be the requested aspect ratio
  19. if a > A, padding is done on top and bottom
  20. if a < A, padding is done on left and right
  21. */
  22. return [
  23. /*
  24. In both cases, we first have to scale the input to match the requested size.
  25. When using computed width/height, we truncate them to multiples of 2
  26. */
  27. {
  28. filter: 'scale',
  29. options: {
  30. w: 'if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2)',
  31. h: 'if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)'
  32. }
  33. },
  34. /*
  35. Then we pad the scaled input to match the target size
  36. (here iw and ih refer to the padding input, i.e the scaled output)
  37. */
  38. {
  39. filter: 'pad',
  40. options: {
  41. w: width,
  42. h: height,
  43. x: 'if(gt(a,' + aspect + '),0,(' + width + '-iw)/2)',
  44. y: 'if(lt(a,' + aspect + '),0,(' + height + '-ih)/2)',
  45. color: color
  46. }
  47. }
  48. ];
  49. }
  50. /**
  51. * Recompute size filters
  52. *
  53. * @param {Object} output
  54. * @param {String} key newly-added parameter name ('size', 'aspect' or 'pad')
  55. * @param {String} value newly-added parameter value
  56. * @return filter string array
  57. * @private
  58. */
  59. function createSizeFilters(output, key, value) {
  60. // Store parameters
  61. var data = output.sizeData = output.sizeData || {};
  62. data[key] = value;
  63. if (!('size' in data)) {
  64. // No size requested, keep original size
  65. return [];
  66. }
  67. // Try to match the different size string formats
  68. var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/);
  69. var fixedWidth = data.size.match(/([0-9]+)x\?/);
  70. var fixedHeight = data.size.match(/\?x([0-9]+)/);
  71. var percentRatio = data.size.match(/\b([0-9]{1,3})%/);
  72. var width, height, aspect;
  73. if (percentRatio) {
  74. var ratio = Number(percentRatio[1]) / 100;
  75. return [{
  76. filter: 'scale',
  77. options: {
  78. w: 'trunc(iw*' + ratio + '/2)*2',
  79. h: 'trunc(ih*' + ratio + '/2)*2'
  80. }
  81. }];
  82. } else if (fixedSize) {
  83. // Round target size to multiples of 2
  84. width = Math.round(Number(fixedSize[1]) / 2) * 2;
  85. height = Math.round(Number(fixedSize[2]) / 2) * 2;
  86. aspect = width / height;
  87. if (data.pad) {
  88. return getScalePadFilters(width, height, aspect, data.pad);
  89. } else {
  90. // No autopad requested, rescale to target size
  91. return [{ filter: 'scale', options: { w: width, h: height }}];
  92. }
  93. } else if (fixedWidth || fixedHeight) {
  94. if ('aspect' in data) {
  95. // Specified aspect ratio
  96. width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect);
  97. height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect);
  98. // Round to multiples of 2
  99. width = Math.round(width / 2) * 2;
  100. height = Math.round(height / 2) * 2;
  101. if (data.pad) {
  102. return getScalePadFilters(width, height, data.aspect, data.pad);
  103. } else {
  104. // No autopad requested, rescale to target size
  105. return [{ filter: 'scale', options: { w: width, h: height }}];
  106. }
  107. } else {
  108. // Keep input aspect ratio
  109. if (fixedWidth) {
  110. return [{
  111. filter: 'scale',
  112. options: {
  113. w: Math.round(Number(fixedWidth[1]) / 2) * 2,
  114. h: 'trunc(ow/a/2)*2'
  115. }
  116. }];
  117. } else {
  118. return [{
  119. filter: 'scale',
  120. options: {
  121. w: 'trunc(oh*a/2)*2',
  122. h: Math.round(Number(fixedHeight[1]) / 2) * 2
  123. }
  124. }];
  125. }
  126. }
  127. } else {
  128. throw new Error('Invalid size specified: ' + data.size);
  129. }
  130. }
  131. /*
  132. *! Video size-related methods
  133. */
  134. module.exports = function(proto) {
  135. /**
  136. * Keep display aspect ratio
  137. *
  138. * This method is useful when converting an input with non-square pixels to an output format
  139. * that does not support non-square pixels. It rescales the input so that the display aspect
  140. * ratio is the same.
  141. *
  142. * @method FfmpegCommand#keepDAR
  143. * @category Video size
  144. * @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio
  145. *
  146. * @return FfmpegCommand
  147. */
  148. proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio
  149. proto.keepDisplayAspect =
  150. proto.keepDisplayAspectRatio =
  151. proto.keepDAR = function() {
  152. return this.videoFilters([
  153. {
  154. filter: 'scale',
  155. options: {
  156. w: 'if(gt(sar,1),iw*sar,iw)',
  157. h: 'if(lt(sar,1),ih/sar,ih)'
  158. }
  159. },
  160. {
  161. filter: 'setsar',
  162. options: '1'
  163. }
  164. ]);
  165. };
  166. /**
  167. * Set output size
  168. *
  169. * The 'size' parameter can have one of 4 forms:
  170. * - 'X%': rescale to xx % of the original size
  171. * - 'WxH': specify width and height
  172. * - 'Wx?': specify width and compute height from input aspect ratio
  173. * - '?xH': specify height and compute width from input aspect ratio
  174. *
  175. * Note: both dimensions will be truncated to multiples of 2.
  176. *
  177. * @method FfmpegCommand#size
  178. * @category Video size
  179. * @aliases withSize,setSize
  180. *
  181. * @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240'
  182. * @return FfmpegCommand
  183. */
  184. proto.withSize =
  185. proto.setSize =
  186. proto.size = function(size) {
  187. var filters = createSizeFilters(this._currentOutput, 'size', size);
  188. this._currentOutput.sizeFilters.clear();
  189. this._currentOutput.sizeFilters(filters);
  190. return this;
  191. };
  192. /**
  193. * Set output aspect ratio
  194. *
  195. * @method FfmpegCommand#aspect
  196. * @category Video size
  197. * @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio
  198. *
  199. * @param {String|Number} aspect aspect ratio (number or 'X:Y' string)
  200. * @return FfmpegCommand
  201. */
  202. proto.withAspect =
  203. proto.withAspectRatio =
  204. proto.setAspect =
  205. proto.setAspectRatio =
  206. proto.aspect =
  207. proto.aspectRatio = function(aspect) {
  208. var a = Number(aspect);
  209. if (isNaN(a)) {
  210. var match = aspect.match(/^(\d+):(\d+)$/);
  211. if (match) {
  212. a = Number(match[1]) / Number(match[2]);
  213. } else {
  214. throw new Error('Invalid aspect ratio: ' + aspect);
  215. }
  216. }
  217. var filters = createSizeFilters(this._currentOutput, 'aspect', a);
  218. this._currentOutput.sizeFilters.clear();
  219. this._currentOutput.sizeFilters(filters);
  220. return this;
  221. };
  222. /**
  223. * Enable auto-padding the output
  224. *
  225. * @method FfmpegCommand#autopad
  226. * @category Video size
  227. * @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad
  228. *
  229. * @param {Boolean} [pad=true] enable/disable auto-padding
  230. * @param {String} [color='black'] pad color
  231. */
  232. proto.applyAutopadding =
  233. proto.applyAutoPadding =
  234. proto.applyAutopad =
  235. proto.applyAutoPad =
  236. proto.withAutopadding =
  237. proto.withAutoPadding =
  238. proto.withAutopad =
  239. proto.withAutoPad =
  240. proto.autoPad =
  241. proto.autopad = function(pad, color) {
  242. // Allow autopad(color)
  243. if (typeof pad === 'string') {
  244. color = pad;
  245. pad = true;
  246. }
  247. // Allow autopad() and autopad(undefined, color)
  248. if (typeof pad === 'undefined') {
  249. pad = true;
  250. }
  251. var filters = createSizeFilters(this._currentOutput, 'pad', pad ? color || 'black' : false);
  252. this._currentOutput.sizeFilters.clear();
  253. this._currentOutput.sizeFilters(filters);
  254. return this;
  255. };
  256. };