UNPKG

@distube/ytdl-core

Version:

DisTube fork of ytdl-core. YouTube video downloader in pure javascript.

217 lines (191 loc) 6.76 kB
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?.includes(enc)); const getAudioBitrate = format => format.audioBitrate || 0; const getAudioEncodingRank = format => audioEncodingRanks.findIndex(enc => 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; };