123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- const utils = require('./utils');
- const FORMATS = require('./formats');
- // Use these to help sort formats, higher index is better.
- const audioEncodingRanks = [
- 'mp4a',
- 'mp3',
- 'vorbis',
- 'aac',
- 'opus',
- 'flac',
- ];
- const videoEncodingRanks = [
- 'mp4v',
- 'avc1',
- 'Sorenson H.283',
- 'MPEG-4 Visual',
- 'VP8',
- 'VP9',
- 'H.264',
- ];
- const getVideoBitrate = format => format.bitrate || 0;
- const getVideoEncodingRank = format =>
- videoEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
- const getAudioBitrate = format => format.audioBitrate || 0;
- const getAudioEncodingRank = format =>
- audioEncodingRanks.findIndex(enc => format.codecs && format.codecs.includes(enc));
- /**
- * Sort formats by a list of functions.
- *
- * @param {Object} a
- * @param {Object} b
- * @param {Array.<Function>} sortBy
- * @returns {number}
- */
- const sortFormatsBy = (a, b, sortBy) => {
- let res = 0;
- for (let fn of sortBy) {
- res = fn(b) - fn(a);
- if (res !== 0) {
- break;
- }
- }
- return res;
- };
- const sortFormatsByVideo = (a, b) => sortFormatsBy(a, b, [
- format => parseInt(format.qualityLabel),
- getVideoBitrate,
- getVideoEncodingRank,
- ]);
- const sortFormatsByAudio = (a, b) => sortFormatsBy(a, b, [
- getAudioBitrate,
- getAudioEncodingRank,
- ]);
- /**
- * Sort formats from highest quality to lowest.
- *
- * @param {Object} a
- * @param {Object} b
- * @returns {number}
- */
- exports.sortFormats = (a, b) => sortFormatsBy(a, b, [
- // Formats with both video and audio are ranked highest.
- format => +!!format.isHLS,
- format => +!!format.isDashMPD,
- format => +(format.contentLength > 0),
- format => +(format.hasVideo && format.hasAudio),
- format => +format.hasVideo,
- format => parseInt(format.qualityLabel) || 0,
- getVideoBitrate,
- getAudioBitrate,
- getVideoEncodingRank,
- getAudioEncodingRank,
- ]);
- /**
- * Choose a format depending on the given options.
- *
- * @param {Array.<Object>} formats
- * @param {Object} options
- * @returns {Object}
- * @throws {Error} when no format matches the filter/format rules
- */
- exports.chooseFormat = (formats, options) => {
- if (typeof options.format === 'object') {
- if (!options.format.url) {
- throw Error('Invalid format given, did you use `ytdl.getInfo()`?');
- }
- return options.format;
- }
- if (options.filter) {
- formats = exports.filterFormats(formats, options.filter);
- }
- // We currently only support HLS-Formats for livestreams
- // So we (now) remove all non-HLS streams
- if (formats.some(fmt => fmt.isHLS)) {
- formats = formats.filter(fmt => fmt.isHLS || !fmt.isLive);
- }
- let format;
- const quality = options.quality || 'highest';
- switch (quality) {
- case 'highest':
- format = formats[0];
- break;
- case 'lowest':
- format = formats[formats.length - 1];
- break;
- case 'highestaudio': {
- formats = exports.filterFormats(formats, 'audio');
- formats.sort(sortFormatsByAudio);
- // Filter for only the best audio format
- const bestAudioFormat = formats[0];
- formats = formats.filter(f => sortFormatsByAudio(bestAudioFormat, f) === 0);
- // Check for the worst video quality for the best audio quality and pick according
- // This does not loose default sorting of video encoding and bitrate
- const worstVideoQuality = formats.map(f => parseInt(f.qualityLabel) || 0).sort((a, b) => a - b)[0];
- format = formats.find(f => (parseInt(f.qualityLabel) || 0) === worstVideoQuality);
- break;
- }
- case 'lowestaudio':
- formats = exports.filterFormats(formats, 'audio');
- formats.sort(sortFormatsByAudio);
- format = formats[formats.length - 1];
- break;
- case 'highestvideo': {
- formats = exports.filterFormats(formats, 'video');
- formats.sort(sortFormatsByVideo);
- // Filter for only the best video format
- const bestVideoFormat = formats[0];
- formats = formats.filter(f => sortFormatsByVideo(bestVideoFormat, f) === 0);
- // Check for the worst audio quality for the best video quality and pick according
- // This does not loose default sorting of audio encoding and bitrate
- const worstAudioQuality = formats.map(f => f.audioBitrate || 0).sort((a, b) => a - b)[0];
- format = formats.find(f => (f.audioBitrate || 0) === worstAudioQuality);
- break;
- }
- case 'lowestvideo':
- formats = exports.filterFormats(formats, 'video');
- formats.sort(sortFormatsByVideo);
- format = formats[formats.length - 1];
- break;
- default:
- format = getFormatByQuality(quality, formats);
- break;
- }
- if (!format) {
- throw Error(`No such format found: ${quality}`);
- }
- return format;
- };
- /**
- * Gets a format based on quality or array of quality's
- *
- * @param {string|[string]} quality
- * @param {[Object]} formats
- * @returns {Object}
- */
- const getFormatByQuality = (quality, formats) => {
- let getFormat = itag => formats.find(format => `${format.itag}` === `${itag}`);
- if (Array.isArray(quality)) {
- return getFormat(quality.find(q => getFormat(q)));
- } else {
- return getFormat(quality);
- }
- };
- /**
- * @param {Array.<Object>} formats
- * @param {Function} filter
- * @returns {Array.<Object>}
- */
- exports.filterFormats = (formats, filter) => {
- let fn;
- switch (filter) {
- case 'videoandaudio':
- case 'audioandvideo':
- fn = format => format.hasVideo && format.hasAudio;
- break;
- case 'video':
- fn = format => format.hasVideo;
- break;
- case 'videoonly':
- fn = format => format.hasVideo && !format.hasAudio;
- break;
- case 'audio':
- fn = format => format.hasAudio;
- break;
- case 'audioonly':
- fn = format => !format.hasVideo && format.hasAudio;
- break;
- default:
- if (typeof filter === 'function') {
- fn = filter;
- } else {
- throw TypeError(`Given filter (${filter}) is not supported`);
- }
- }
- return formats.filter(format => !!format.url && fn(format));
- };
- /**
- * @param {Object} format
- * @returns {Object}
- */
- exports.addFormatMeta = format => {
- format = Object.assign({}, FORMATS[format.itag], format);
- format.hasVideo = !!format.qualityLabel;
- format.hasAudio = !!format.audioBitrate;
- format.container = format.mimeType ?
- format.mimeType.split(';')[0].split('/')[1] : null;
- format.codecs = format.mimeType ?
- utils.between(format.mimeType, 'codecs="', '"') : null;
- format.videoCodec = format.hasVideo && format.codecs ?
- format.codecs.split(', ')[0] : null;
- format.audioCodec = format.hasAudio && format.codecs ?
- format.codecs.split(', ').slice(-1)[0] : null;
- format.isLive = /\bsource[/=]yt_live_broadcast\b/.test(format.url);
- format.isHLS = /\/manifest\/hls_(variant|playlist)\//.test(format.url);
- format.isDashMPD = /\/manifest\/dash\//.test(format.url);
- return format;
- };
|