UNPKG

yt-stream

Version:

Create easily readable streams from YouTube video url's

157 lines (140 loc) 5.71 kB
const { Readable } = require('stream'); const { EventEmitter } = require('events'); const { URL } = require('url'); const cipher = require('../stream/decipher.js'); const { requestCallback, request } = require('../request/index.js'); const { YTStreamAgent } = require('../cookieHandler.js'); const getInfo = require('../info.js').getInfo; function parseAudio(formats){ const audio = []; var audioFormats = formats.filter(f => f.mimeType.startsWith('audio')); for(var i = 0; i < audioFormats.length; i++){ var format = audioFormats[i]; const type = format.mimeType; if(type.startsWith('audio')){ format.codec = type.split('codecs=')[1].split('"')[0]; format.container = type.split('audio/')[1].split(';')[0]; audio.push(format); } } return audio; } function parseVideo(formats){ const video = []; var videoFormats = formats.filter(f => f.mimeType.startsWith('video')); for(var i = 0; i < videoFormats.length; i++){ var format = videoFormats[i]; const type = format.mimeType; if(type.startsWith('video')){ format.codec = type.split('codecs=')[1].split('"')[0]; format.container = type.split('video/')[1].split(';')[0]; video.push(format); } } return video; } class Stream extends EventEmitter{ constructor(ytstream, url, options, info){ super(); this.stream = new Readable({highWaterMark: (options.highWaterMark || 1048576 * 32), read() {}}); this.container = options.container; this.ytstream = ytstream; this.url = url; this.video_url = options.video_url; this.quality = options.quality; this.info = info; this.mimeType = options.format.mimeType; this.bytes_count = 0; this.content_length = parseInt(options.contentLength); this.duration = options.duration; this.type = options.type; this.req_type = options.req_type; this.per_sec_byte = Math.ceil(this.content_length / this.duration); this.retryCount = 0; this.format = options.format; if(options.download === true) this.loop(); else { this.emit('ready'); this.ready = true; } } async retry(){ if(this.ytstream.agent instanceof YTStreamAgent) this.ytstream.agent.removeCookies(false); const info = await getInfo(this.ytstream, this.video_url, true); const _ci = await cipher.format_decipher(info.formats, info.cver, info.html5player, this.ytstream.agent); info.formats = _ci; var audioFormat = this.req_type === 'video' ? parseVideo(info.formats) : parseAudio(info.formats); if(audioFormat.length === 0) audioFormat = this.req_type === 'video' ? parseAudio(info.formats) : parseVideo(info.formats); this.url = typeof this.quality === 'number' ? (audioFormat[this.quality] ? audioFormat[this.quality].url : audioFormat[audioFormat.length - 1].url) : audioFormat[0].url; this.loop(); } async loop(){ let parsed = new URL(this.url); try{ await request(parsed.protocol+"//"+parsed.host+"/generate_204", { method: 'GET' }, this.ytstream.agent, 0, true); } catch { ++this.retryCount; if(this.retryCount >= 5){ return this.emit('error', 'Failed to get valid content'); } else return this.loop(); } requestCallback(this.url, { headers: { range: `bytes=0-${this.content_length}` }, method: 'GET' }, this.ytstream.agent, false).then(async ({stream, req}) => { this.req = req; if(Number(stream.statusCode) >= 400){ if(this.retryCount >= 5){ return this.emit('error', 'No valid download url\'s could be found for the YouTube video'); } else { ++this.retryCount; return await this.retry(); } } else if(Number(stream.statusCode) >= 300 && Object.keys(stream.headers).map(h => h.toLowerCase()).indexOf('location') >= 0){ ++this.retryCount; const headerKeys = Object.keys(stream.headers).map(h => h.toLowerCase()); const headerValues = Object.values(stream.headers); this.url = headerValues[headerKeys.indexOf('location')]; return this.loop(); } var chunkCount = 0; stream.on('data', chunk => { this.bytes_count += chunk.length; this.stream.push(chunk); ++chunkCount; if(chunkCount === 3){ this.emit('ready'); this.ready = true; } }); stream.on('end', () => { if(chunkCount < 3){ this.emit('ready'); this.ready = true; } this.stream.push(null); }); stream.on('error', err => { this.emit('error', err); }); }).catch(err => { this.emit('error', err); }); } pause(){ this.stream.pause(); } resume(){ this.stream.resume(); } destroy(){ this.req.destroy(); this.stream.destroy(); } ready = false; } module.exports = Stream;