diff --git a/README.md b/README.md index e69bd130..e229373c 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ ytdl('http://www.youtube.com/watch?v=aqz-KE-bpKQ') # API ### ytdl(url, [options]) -Attempts to download a video from the given url. Returns a [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable). `options` can have the following, in addition to any [`getInfo()` option](#async-ytdl.getinfo(url%2C-%5Boptions%5D)) and [`chooseFormat()` option](#ytdl.downloadfrominfo(info%2C-options)). +Attempts to download a video from the given url. Returns a [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable). `options` can have the following, in addition to any [`getInfo()` option](#async-ytdlgetinfourl-options) and [`chooseFormat()` option](#ytdlchooseformatformats-options). * `range` - A byte range in the form `{start: INT, end: INT}` that specifies part of the file to download, ie {start: 10355705, end: 12452856}. Not supported on segmented (DASH MPD, m3u8) formats. * This downloads a portion of the file, and not a separately spliced video. * `begin` - What time in the video to begin. Supports formats `00:00:00.000`, `0ms, 0s, 0m, 0h`, or number of milliseconds. Example: `1:30`, `05:10.123`, `10m30s`. * For live videos, this also accepts a unix timestamp or Date object, and defaults to `Date.now()`. - * This option is not very reliable for non-live videos, see [#129](https://github.com/fent/node-ytdl-core/issues/129), [#219](https://github.com/fent/node-ytdl-core/issues/219). + * This option is not very reliable for non-live videos, see [#129](https://github.com/fent/node-ytdl-core/issues/129) and [#219](https://github.com/fent/node-ytdl-core/issues/219). * `liveBuffer` - How much time buffer to use for live videos in milliseconds. Default is `20000`. * `highWaterMark` - How much of the video download to buffer into memory. See [node's docs](https://nodejs.org/api/stream.html#stream_constructor_new_stream_writable_options) for more. Defaults to 512KB. * `dlChunkSize` - When the chosen format is video only or audio only, the download is separated into multiple chunks to avoid throttling. This option specifies the size of each chunk in bytes. Setting it to 0 disables chunking. Defaults to 10MB. @@ -81,7 +81,7 @@ Can be used if you'd like to choose a format yourself. Throws an Error if it fai `options` can have the following -* `quality` - Video quality to download. Can be an [itag value](http://en.wikipedia.org/wiki/YouTube#Quality_and_formats), a list of itag values, or `highest`/`lowest`/`highestaudio`/`lowestaudio`/`highestvideo`/`lowestvideo`. `highestaudio`/`lowestaudio`/`highestvideo`/`lowestvideo` all prefer audio/video only respectively. Defaults to `highest`, which prefers formats with both video and audio. +* `quality` - Video quality to download. Can be an [itag value](http://en.wikipedia.org/wiki/YouTube#Quality_and_formats), a list of itag values, or one of these strings: `highest`/`lowest`/`highestaudio`/`lowestaudio`/`highestvideo`/`lowestvideo`. `highestaudio`/`lowestaudio` try to minimize video bitrate for equally good audio formats while `highestvideo`/`lowestvideo` try to minimize audio respectively. Defaults to `highest`, which prefers formats with both video and audio. A typical video's formats will be sorted in the following way using `quality: 'highest'` ``` @@ -151,6 +151,7 @@ ytdl cannot download videos that fall into the following * Private (if you have access, requires [cookies](example/cookies.js)) * Rentals (if you have access, requires [cookies](example/cookies.js)) * YouTube Premium content (if you have access, requires [cookies](example/cookies.js)) +* Only [HLS Livestreams](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) are currently supported. Other formats will get filtered out in ytdl.chooseFormats Generated download links are valid for 6 hours, and may only be downloadable from the same IP address. diff --git a/lib/format-utils.js b/lib/format-utils.js index c5767653..276641f8 100644 --- a/lib/format-utils.js +++ b/lib/format-utils.js @@ -104,6 +104,12 @@ exports.chooseFormat = (formats, options) => { 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) { @@ -115,11 +121,18 @@ exports.chooseFormat = (formats, options) => { format = formats[formats.length - 1]; break; - case 'highestaudio': + case 'highestaudio': { formats = exports.filterFormats(formats, 'audio'); formats.sort(sortFormatsByAudio); - format = formats[0]; + // 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'); @@ -127,11 +140,18 @@ exports.chooseFormat = (formats, options) => { format = formats[formats.length - 1]; break; - case 'highestvideo': + case 'highestvideo': { formats = exports.filterFormats(formats, 'video'); formats.sort(sortFormatsByVideo); - format = formats[0]; + // 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'); diff --git a/test/format-utils-test.js b/test/format-utils-test.js index f2ac7396..4f77b2ab 100644 --- a/test/format-utils-test.js +++ b/test/format-utils-test.js @@ -147,6 +147,100 @@ const formats = [ hasAudio: false, }, ]; + +const liveWithHLS = formats.filter(x => x.isLive).slice(); +liveWithHLS.push( + { + itag: '96', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '1080p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 2500000, + audioBitrate: 256, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, + { + itag: '96.worse.audio', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '1080p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 2500000, + audioBitrate: 128, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, + { + itag: '95', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '720p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 1500000, + audioBitrate: 256, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, + { + itag: '94', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '480p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 800000, + audioBitrate: 128, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, + { + itag: '92', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '240p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 150000, + audioBitrate: 48, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, + { + itag: '91', + mimeType: 'video/ts; codecs="H.264, aac"', + container: 'ts', + qualityLabel: '144p', + codecs: 'H.264, aac', + videoCodec: 'H.264', + audioCodec: 'aac', + bitrate: 100000, + audioBitrate: 48, + url: 'https://googlevideo.com/', + hasVideo: true, + hasAudio: true, + isHLS: true, + }, +); const getItags = format => format.itag; @@ -191,6 +285,21 @@ describe('chooseFormat', () => { const format = chooseFormat(formats, { quality: 'highestaudio' }); assert.strictEqual(format.itag, '43'); }); + + describe('and no formats passed', () => { + it('throws the regular no such format found error', () => { + assert.throws(() => { + chooseFormat([], { quality: 'highestaudio' }); + }, /No such format found/); + }); + }); + + describe('and HLS formats are present', () => { + it('Chooses highest audio itag', () => { + const format = chooseFormat(liveWithHLS, { quality: 'highestaudio' }); + assert.strictEqual(format.itag, '95'); + }); + }); }); describe('With lowest audio quality wanted', () => { @@ -198,6 +307,13 @@ describe('chooseFormat', () => { const format = chooseFormat(formats, { quality: 'lowestaudio' }); assert.strictEqual(format.itag, '17'); }); + + describe('and HLS formats are present', () => { + it('Chooses lowest audio itag', () => { + const format = chooseFormat(liveWithHLS, { quality: 'lowestaudio' }); + assert.strictEqual(format.itag, '91'); + }); + }); }); describe('With highest video quality wanted', () => { @@ -205,6 +321,21 @@ describe('chooseFormat', () => { const format = chooseFormat(formats, { quality: 'highestvideo' }); assert.strictEqual(format.itag, '18'); }); + + describe('and no formats passed', () => { + it('throws the regular no such format found error', () => { + assert.throws(() => { + chooseFormat([], { quality: 'highestvideo' }); + }, /No such format found/); + }); + }); + + describe('and HLS formats are present', () => { + it('Chooses highest video itag', () => { + const format = chooseFormat(liveWithHLS, { quality: 'highestvideo' }); + assert.strictEqual(format.itag, '96.worse.audio'); + }); + }); }); describe('With lowest video quality wanted', () => { @@ -212,6 +343,13 @@ describe('chooseFormat', () => { const format = chooseFormat(formats, { quality: 'lowestvideo' }); assert.strictEqual(format.itag, '17'); }); + + describe('and HLS formats are present', () => { + it('Chooses lowest audio itag', () => { + const format = chooseFormat(liveWithHLS, { quality: 'lowestvideo' }); + assert.strictEqual(format.itag, '91'); + }); + }); }); describe('With itag given', () => { @@ -261,6 +399,16 @@ describe('chooseFormat', () => { }); }); + describe('that matches audio only formats', () => { + describe('and only non-HLS-livestream would match', () => { + it('throws the no format found exception', () => { + assert.throws(() => { + chooseFormat(liveWithHLS, { quality: 'audioonly' }); + }, /No such format found/); + }); + }); + }); + describe('that does not match a format', () => { it('Returns an error', () => { assert.throws(() => {