format-utils.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. const utils = require('./utils');
  2. const FORMATS = require('./formats');
  3. // Use these to help sort formats, higher index is better.
  4. const audioEncodingRanks = [
  5. 'mp4a',
  6. 'mp3',
  7. 'vorbis',
  8. 'aac',
  9. 'opus',
  10. 'flac',
  11. ];
  12. const videoEncodingRanks = [
  13. 'mp4v',
  14. 'avc1',
  15. 'Sorenson H.283',
  16. 'MPEG-4 Visual',
  17. 'VP8',
  18. 'VP9',
  19. 'H.264',
  20. ];
  21. const getVideoBitrate = format => format.bitrate || 0;
  22. const getVideoEncodingRank = format =>
  23. videoEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
  24. const getAudioBitrate = format => format.audioBitrate || 0;
  25. const getAudioEncodingRank = format =>
  26. audioEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
  27. /**
  28. * Sort formats by a list of functions.
  29. *
  30. * @param {Object} a
  31. * @param {Object} b
  32. * @param {Array.<Function>} sortBy
  33. * @returns {number}
  34. */
  35. const sortFormatsBy = (a, b, sortBy) => {
  36. let res = 0;
  37. for (let fn of sortBy) {
  38. res = fn(b) - fn(a);
  39. if (res !== 0) {
  40. break;
  41. }
  42. }
  43. return res;
  44. };
  45. const sortFormatsByVideo = (a, b) => sortFormatsBy(a, b, [
  46. format => parseInt(format.qualityLabel),
  47. getVideoBitrate,
  48. getVideoEncodingRank,
  49. ]);
  50. const sortFormatsByAudio = (a, b) => sortFormatsBy(a, b, [
  51. getAudioBitrate,
  52. getAudioEncodingRank,
  53. ]);
  54. /**
  55. * Sort formats from highest quality to lowest.
  56. *
  57. * @param {Object} a
  58. * @param {Object} b
  59. * @returns {number}
  60. */
  61. exports.sortFormats = (a, b) => sortFormatsBy(a, b, [
  62. // Formats with both video and audio are ranked highest.
  63. format => +!!format.isHLS,
  64. format => +!!format.isDashMPD,
  65. format => +(format.contentLength > 0),
  66. format => +(format.hasVideo && format.hasAudio),
  67. format => +format.hasVideo,
  68. format => parseInt(format.qualityLabel) || 0,
  69. getVideoBitrate,
  70. getAudioBitrate,
  71. getVideoEncodingRank,
  72. getAudioEncodingRank,
  73. ]);
  74. /**
  75. * Choose a format depending on the given options.
  76. *
  77. * @param {Array.<Object>} formats
  78. * @param {Object} options
  79. * @returns {Object}
  80. * @throws {Error} when no format matches the filter/format rules
  81. */
  82. exports.chooseFormat = (formats, options) => {
  83. if (typeof options.format === 'object') {
  84. if (!options.format.url) {
  85. throw Error('Invalid format given, did you use `ytdl.getInfo()`?');
  86. }
  87. return options.format;
  88. }
  89. if (options.filter) {
  90. formats = exports.filterFormats(formats, options.filter);
  91. }
  92. // We currently only support HLS-Formats for livestreams
  93. // So we (now) remove all non-HLS streams
  94. if (formats.some(fmt => fmt.isHLS)) {
  95. formats = formats.filter(fmt => fmt.isHLS || !fmt.isLive);
  96. }
  97. let format;
  98. const quality = options.quality || 'highest';
  99. switch (quality) {
  100. case 'highest':
  101. format = formats[0];
  102. break;
  103. case 'lowest':
  104. format = formats[formats.length - 1];
  105. break;
  106. case 'highestaudio': {
  107. formats = exports.filterFormats(formats, 'audio');
  108. formats.sort(sortFormatsByAudio);
  109. // Filter for only the best audio format
  110. const bestAudioFormat = formats[0];
  111. formats = formats.filter(f => sortFormatsByAudio(bestAudioFormat, f) === 0);
  112. // Check for the worst video quality for the best audio quality and pick according
  113. // This does not loose default sorting of video encoding and bitrate
  114. const worstVideoQuality = formats.map(f => parseInt(f.qualityLabel) || 0).sort((a, b) => a - b)[0];
  115. format = formats.find(f => (parseInt(f.qualityLabel) || 0) === worstVideoQuality);
  116. break;
  117. }
  118. case 'lowestaudio':
  119. formats = exports.filterFormats(formats, 'audio');
  120. formats.sort(sortFormatsByAudio);
  121. format = formats[formats.length - 1];
  122. break;
  123. case 'highestvideo': {
  124. formats = exports.filterFormats(formats, 'video');
  125. formats.sort(sortFormatsByVideo);
  126. // Filter for only the best video format
  127. const bestVideoFormat = formats[0];
  128. formats = formats.filter(f => sortFormatsByVideo(bestVideoFormat, f) === 0);
  129. // Check for the worst audio quality for the best video quality and pick according
  130. // This does not loose default sorting of audio encoding and bitrate
  131. const worstAudioQuality = formats.map(f => f.audioBitrate || 0).sort((a, b) => a - b)[0];
  132. format = formats.find(f => (f.audioBitrate || 0) === worstAudioQuality);
  133. break;
  134. }
  135. case 'lowestvideo':
  136. formats = exports.filterFormats(formats, 'video');
  137. formats.sort(sortFormatsByVideo);
  138. format = formats[formats.length - 1];
  139. break;
  140. default:
  141. format = getFormatByQuality(quality, formats);
  142. break;
  143. }
  144. if (!format) {
  145. throw Error(`No such format found: ${quality}`);
  146. }
  147. return format;
  148. };
  149. /**
  150. * Gets a format based on quality or array of quality's
  151. *
  152. * @param {string|[string]} quality
  153. * @param {[Object]} formats
  154. * @returns {Object}
  155. */
  156. const getFormatByQuality = (quality, formats) => {
  157. let getFormat = itag => formats.find(format => `${format.itag}` === `${itag}`);
  158. if (Array.isArray(quality)) {
  159. return getFormat(quality.find(q => getFormat(q)));
  160. } else {
  161. return getFormat(quality);
  162. }
  163. };
  164. /**
  165. * @param {Array.<Object>} formats
  166. * @param {Function} filter
  167. * @returns {Array.<Object>}
  168. */
  169. exports.filterFormats = (formats, filter) => {
  170. let fn;
  171. switch (filter) {
  172. case 'videoandaudio':
  173. case 'audioandvideo':
  174. fn = format => format.hasVideo && format.hasAudio;
  175. break;
  176. case 'video':
  177. fn = format => format.hasVideo;
  178. break;
  179. case 'videoonly':
  180. fn = format => format.hasVideo && !format.hasAudio;
  181. break;
  182. case 'audio':
  183. fn = format => format.hasAudio;
  184. break;
  185. case 'audioonly':
  186. fn = format => !format.hasVideo && format.hasAudio;
  187. break;
  188. default:
  189. if (typeof filter === 'function') {
  190. fn = filter;
  191. } else {
  192. throw TypeError(`Given filter (${filter}) is not supported`);
  193. }
  194. }
  195. return formats.filter(format => !!format.url && fn(format));
  196. };
  197. /**
  198. * @param {Object} format
  199. * @returns {Object}
  200. */
  201. exports.addFormatMeta = format => {
  202. format = Object.assign({}, FORMATS[format.itag], format);
  203. format.hasVideo = !!format.qualityLabel;
  204. format.hasAudio = !!format.audioBitrate;
  205. format.container = format.mimeType ?
  206. format.mimeType.split(';')[0].split('/')[1] : null;
  207. format.codecs = format.mimeType ?
  208. utils.between(format.mimeType, 'codecs="', '"') : null;
  209. format.videoCodec = format.hasVideo && format.codecs ?
  210. format.codecs.split(', ')[0] : null;
  211. format.audioCodec = format.hasAudio && format.codecs ?
  212. format.codecs.split(', ').slice(-1)[0] : null;
  213. format.isLive = /\bsource[/=]yt_live_broadcast\b/.test(format.url);
  214. format.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(format.url);
  215. format.isDashMPD = /\/manifest\/dash\//.test(format.url);
  216. return format;
  217. };