UNPKG

6.16 kBJavaScriptView Raw
1const PassThrough = require('stream').PassThrough;
2const getInfo = require('./info');
3const utils = require('./utils');
4const formatUtils = require('./format-utils');
5const urlUtils = require('./url-utils');
6const sig = require('./sig');
7const miniget = require('miniget');
8const m3u8stream = require('m3u8stream');
9const parseTime = require('m3u8stream/dist/parse-time');
10
11
12/**
13 * @param {string} link
14 * @param {!Object} options
15 * @returns {ReadableStream}
16 */
17const ytdl = (link, options) => {
18 const stream = createStream(options);
19 ytdl.getInfo(link, options).then(info => {
20 downloadFromInfoCallback(stream, info, options);
21 }, stream.emit.bind(stream, 'error'));
22 return stream;
23};
24module.exports = ytdl;
25
26ytdl.getBasicInfo = getInfo.getBasicInfo;
27ytdl.getInfo = getInfo.getInfo;
28ytdl.chooseFormat = formatUtils.chooseFormat;
29ytdl.filterFormats = formatUtils.filterFormats;
30ytdl.validateID = urlUtils.validateID;
31ytdl.validateURL = urlUtils.validateURL;
32ytdl.getURLVideoID = urlUtils.getURLVideoID;
33ytdl.getVideoID = urlUtils.getVideoID;
34ytdl.cache = {
35 sig: sig.cache,
36 info: getInfo.cache,
37 cookie: getInfo.cookieCache,
38};
39
40
41const createStream = options => {
42 const stream = new PassThrough({
43 highWaterMark: (options && options.highWaterMark) || 1024 * 512,
44 });
45 stream.destroy = () => { stream._isDestroyed = true; };
46 return stream;
47};
48
49
50const pipeAndSetEvents = (req, stream, end) => {
51 // Forward events from the request to the stream.
52 [
53 'abort', 'request', 'response', 'error', 'redirect', 'retry', 'reconnect',
54 ].forEach(event => {
55 req.prependListener(event, stream.emit.bind(stream, event));
56 });
57 req.pipe(stream, { end });
58};
59
60
61/**
62 * Chooses a format to download.
63 *
64 * @param {stream.Readable} stream
65 * @param {Object} info
66 * @param {Object} options
67 */
68const downloadFromInfoCallback = (stream, info, options) => {
69 options = options || {};
70
71 let err = utils.playError(info, ['UNPLAYABLE', 'LIVE_STREAM_OFFLINE']);
72 if (err) {
73 stream.emit('error', err);
74 return;
75 }
76
77 if (!info.formats.length) {
78 stream.emit('error', Error('This video is unavailable'));
79 return;
80 }
81
82 let format;
83 try {
84 format = formatUtils.chooseFormat(info.formats, options);
85 } catch (e) {
86 stream.emit('error', e);
87 return;
88 }
89 stream.emit('info', info, format);
90 if (stream._isDestroyed) { return; }
91
92 let contentLength, downloaded = 0;
93 const ondata = chunk => {
94 downloaded += chunk.length;
95 stream.emit('progress', chunk.length, downloaded, contentLength);
96 };
97
98 // Download the file in chunks, in this case the default is 10MB,
99 // anything over this will cause youtube to throttle the download
100 const dlChunkSize = options.dlChunkSize || 1024 * 1024 * 10;
101 let req;
102 let shouldEnd = true;
103
104 if (format.isHLS || format.isDashMPD) {
105 req = m3u8stream(format.url, {
106 chunkReadahead: +info.live_chunk_readahead,
107 begin: options.begin || (format.isLive && Date.now()),
108 liveBuffer: options.liveBuffer,
109 requestOptions: options.requestOptions,
110 parser: format.isDashMPD ? 'dash-mpd' : 'm3u8',
111 id: format.itag,
112 });
113
114 req.on('progress', (segment, totalSegments) => {
115 stream.emit('progress', segment.size, segment.num, totalSegments);
116 });
117 pipeAndSetEvents(req, stream, shouldEnd);
118 } else {
119 const requestOptions = Object.assign({}, options.requestOptions, {
120 maxReconnects: 6,
121 maxRetries: 3,
122 backoff: { inc: 500, max: 10000 },
123 });
124
125 let shouldBeChunked = dlChunkSize !== 0 && (!format.hasAudio || !format.hasVideo);
126
127 if (shouldBeChunked) {
128 let start = (options.range && options.range.start) || 0;
129 let end = start + dlChunkSize;
130 const rangeEnd = options.range && options.range.end;
131
132 contentLength = options.range ?
133 (rangeEnd ? rangeEnd + 1 : parseInt(format.contentLength)) - start :
134 parseInt(format.contentLength);
135
136 const getNextChunk = () => {
137 if (!rangeEnd && end >= contentLength) end = 0;
138 if (rangeEnd && end > rangeEnd) end = rangeEnd;
139 shouldEnd = !end || end === rangeEnd;
140
141 requestOptions.headers = Object.assign({}, requestOptions.headers, {
142 Range: `bytes=${start}-${end || ''}`,
143 });
144
145 req = miniget(format.url, requestOptions);
146 req.on('data', ondata);
147 req.on('end', () => {
148 if (stream._isDestroyed) { return; }
149 if (end && end !== rangeEnd) {
150 start = end + 1;
151 end += dlChunkSize;
152 getNextChunk();
153 }
154 });
155 pipeAndSetEvents(req, stream, shouldEnd);
156 };
157 getNextChunk();
158 } else {
159 // Audio only and video only formats don't support begin
160 if (options.begin) {
161 format.url += `&begin=${parseTime.humanStr(options.begin)}`;
162 }
163 if (options.range && (options.range.start || options.range.end)) {
164 requestOptions.headers = Object.assign({}, requestOptions.headers, {
165 Range: `bytes=${options.range.start || '0'}-${options.range.end || ''}`,
166 });
167 }
168 req = miniget(format.url, requestOptions);
169 req.on('response', res => {
170 if (stream._isDestroyed) { return; }
171 contentLength = contentLength || parseInt(res.headers['content-length']);
172 });
173 req.on('data', ondata);
174 pipeAndSetEvents(req, stream, shouldEnd);
175 }
176 }
177
178 stream.destroy = () => {
179 stream._isDestroyed = true;
180 if (req.abort) req.abort();
181 req.end();
182 req.removeListener('data', ondata);
183 req.unpipe();
184 };
185};
186
187
188/**
189 * Can be used to download video after its `info` is gotten through
190 * `ytdl.getInfo()`. In case the user might want to look at the
191 * `info` object before deciding to download.
192 *
193 * @param {Object} info
194 * @param {!Object} options
195 * @returns {ReadableStream}
196 */
197ytdl.downloadFromInfo = (info, options) => {
198 const stream = createStream(options);
199 if (!info.full) {
200 throw Error('Cannot use `ytdl.downloadFromInfo()` when called ' +
201 'with info from `ytdl.getBasicInfo()`');
202 }
203 setImmediate(() => {
204 downloadFromInfoCallback(stream, info, options);
205 });
206 return stream;
207};