UNPKG

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