diff --git a/examples/play[LiveStream(search)].js b/examples/play[LiveStream(search)].js index 4aa6279..8413dd0 100644 --- a/examples/play[LiveStream(search)].js +++ b/examples/play[LiveStream(search)].js @@ -18,21 +18,6 @@ client.on('messageCreate', async message => { let args = message.content.split('play ')[1] let yt_info = await youtube.search(args) let stream = await youtube.stream(yt_info[0].url) - /* - OR if you want to stream Live Video have less delay - - let stream = await youtube.stream(yt_info[0].url, { low_latency : true }) - - OR if you want higher quality audio Live Stream - - let stream = await youtube.stream(yt_info[0].url, { preferred_quality : "480p"}) // You can have resolution upto 1080p - - Default : preferred_quality : "144p" - - OR both - - let stream = await youtube.stream(yt_info[0].url, { low_latency : true ,preferred_quality : "480p"}) - */ let resource = createAudioResource(stream.stream, { inputType : stream.type diff --git a/examples/play[LiveStream(url)].js b/examples/play[LiveStream(url)].js index c0dd13f..6fae71a 100644 --- a/examples/play[LiveStream(url)].js +++ b/examples/play[LiveStream(url)].js @@ -17,21 +17,6 @@ client.on('messageCreate', async message => { let args = message.content.split('play ')[1].split(' ')[0] let stream = await youtube.stream(args) - /* - OR if you want to stream Live Video have less delay - - let stream = await youtube.stream(args, { low_latency : true }) - - OR if you want higher quality audio Live Stream - - let stream = await youtube.stream(args, { preferred_quality : "480p"}) // You can have resolution upto 1080p - - Default : preferred_quality : "144p" - - OR both - - let stream = await youtube.stream(args, { low_latency : true ,preferred_quality : "480p"}) - */ let resource = createAudioResource(stream.stream, { inputType : stream.type diff --git a/package-lock.json b/package-lock.json index abdbdd0..111d94c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "play-dl", - "version": "0.5.6", + "version": "0.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "play-dl", - "version": "0.5.6", + "version": "0.6.0", "license": "MIT", "dependencies": { "got": "^11.8.2" diff --git a/package.json b/package.json index ca67ebc..ceac69c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "play-dl", - "version": "0.5.6", + "version": "0.6.0", "description": "YouTube, SoundCloud, Spotify streaming for discord.js bots", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/play-dl/YouTube/classes/LiveStream.ts b/play-dl/YouTube/classes/LiveStream.ts index d3a73f5..8e3cef8 100644 --- a/play-dl/YouTube/classes/LiveStream.ts +++ b/play-dl/YouTube/classes/LiveStream.ts @@ -12,92 +12,108 @@ export interface FormatInterface{ export class LiveStreaming{ type : StreamType stream : PassThrough - private low_latency : boolean; - private format : FormatInterface + private base_url : string + private url : string private interval : number private packet_count : number private timer : NodeJS.Timer | null private segments_urls : string[] - constructor(format : FormatInterface, low_latency : boolean){ + constructor(dash_url : string, target_interval : number){ this.type = StreamType.Arbitrary - this.low_latency = low_latency || false - this.format = format + this.url = dash_url + this.base_url = '' this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) this.segments_urls = [] this.packet_count = 0 - this.interval = (this.format.targetDurationSec / 2) * 1000 || 0 this.timer = null + this.interval = target_interval * 1000 || 0 this.stream.on('close', () => { this.cleanup() }); - (this.low_latency) ? this.live_loop() :this.start() + this.start() } - private async live_loop(){ - if(this.stream.destroyed) { - this.cleanup() - return - } - await this.manifest_getter() - this.segments_urls.splice(0, this.segments_urls.length - 2) - if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('index.m3u8/sq/')[1].split('/')[0]) - for await (let url of this.segments_urls){ - await (async () => { - return new Promise(async (resolve, reject) => { - if(Number(url.split('index.m3u8/sq/')[1].split('/')[0]) !== this.packet_count){ - resolve('') - return - } - let stream = this.got_stream(url) - stream.on('data', (chunk) => this.stream.write(chunk)) - stream.on('end', () => { - this.packet_count++ - resolve('') - }) - }) - })() - } - this.timer = setTimeout(async () => { - await this.looping() - }, this.interval) - } - - private async looping(){ - if(this.stream.destroyed){ - this.cleanup() - return - } - await this.manifest_getter() - this.segments_urls.splice(0, (this.segments_urls.length / 2)) - for await (let url of this.segments_urls){ - await (async () => { - return new Promise(async (resolve, reject) => { - if(Number(url.split('index.m3u8/sq/')[1].split('/')[0]) !== this.packet_count){ - resolve('') - return - } - let stream = this.got_stream(url) - stream.on('data', (chunk) => this.stream.write(chunk)) - stream.on('end', () => { - this.packet_count++ - resolve('') - }) - }) - })() - } - this.timer = setTimeout(async () => { - await this.looping() - }, this.interval) - } - - private async manifest_getter(){ - let response = await got(this.format.url) - this.segments_urls = response.body.split('\n').filter((x) => x.startsWith('https')) + private async dash_getter(){ + let response = await got(this.url) + let audioFormat = response.body.split('')[0].split('') + if(audioFormat[audioFormat.length - 1] === '') audioFormat.pop() + this.base_url = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] + let list = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] + this.segments_urls = list.replace(new RegExp('') + if(this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop() } private cleanup(){ clearTimeout(this.timer as NodeJS.Timer) this.timer = null + this.url = '' + this.base_url = '' + this.segments_urls = [] + this.packet_count = 0 + this.interval = 0 + } + + private async start(){ + if(this.stream.destroyed){ + this.cleanup() + return + } + await this.dash_getter() + if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0]) + for await (let segment of this.segments_urls){ + if(Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count){ + continue + } + await (async () => { + return new Promise(async (resolve, reject) => { + let stream = got.stream(this.base_url + segment) + stream.on('data', (chunk) => this.stream.write(chunk)) + stream.on('end', () => { + this.packet_count++ + resolve('') + }) + }) + })() + } + this.timer = setTimeout(() => { + this.start() + }, this.interval) + } +} + +export class LiveEnded{ + type : StreamType + stream : PassThrough + private url : string; + private base_url : string; + private packet_count : number + private segments_urls : string[] + constructor(dash_url : string){ + this.type = StreamType.Arbitrary + this.url = dash_url + this.base_url = '' + this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) + this.segments_urls = [] + this.packet_count = 0 + this.stream.on('close', () => { + this.cleanup() + }) + this.start() + } + + private async dash_getter(){ + let response = await got(this.url) + let audioFormat = response.body.split('')[0].split('') + if(audioFormat[audioFormat.length - 1] === '') audioFormat.pop() + this.base_url = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] + let list = audioFormat[audioFormat.length - 1].split('')[1].split('')[0] + this.segments_urls = list.replace(new RegExp('') + if(this.segments_urls[this.segments_urls.length - 1] === '') this.segments_urls.pop() + } + + private cleanup(){ + this.url = '' + this.base_url = '' this.segments_urls = [] this.packet_count = 0 } @@ -107,16 +123,15 @@ export class LiveStreaming{ this.cleanup() return } - await this.manifest_getter() - if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('index.m3u8/sq/')[1].split('/')[0]) - for await (let url of this.segments_urls){ + await this.dash_getter() + if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('sq/')[1].split('/')[0]) + for await (let segment of this.segments_urls){ + if(Number(segment.split('sq/')[1].split('/')[0]) !== this.packet_count){ + continue + } await (async () => { return new Promise(async (resolve, reject) => { - if(Number(url.split('index.m3u8/sq/')[1].split('/')[0]) !== this.packet_count){ - resolve('') - return - } - let stream = this.got_stream(url) + let stream = got.stream(this.base_url + segment) stream.on('data', (chunk) => this.stream.write(chunk)) stream.on('end', () => { this.packet_count++ @@ -125,71 +140,6 @@ export class LiveStreaming{ }) })() } - this.timer = setTimeout(async () => { - await this.start() - }, this.interval) - } - - private got_stream(url: string){ - return got.stream(url) - } -} - -export class LiveEnded{ - type : StreamType - stream : PassThrough - private format : FormatInterface - private packet_count : number - private segments_urls : string[] - constructor(format : FormatInterface){ - this.type = StreamType.Arbitrary - this.format = format - this.stream = new PassThrough({ highWaterMark : 10 * 1000 * 1000 }) - this.segments_urls = [] - this.packet_count = 0 - this.stream.on('close', () => { - this.cleanup() - }) - this.start() - } - - async manifest_getter(){ - let response = await got(this.format.url) - this.segments_urls = response.body.split('\n').filter((x) => x.startsWith('https')) - } - - private cleanup(){ - this.segments_urls = [] - this.packet_count = 0 - } - - async start(){ - if(this.stream.destroyed){ - this.cleanup() - return - } - await this.manifest_getter() - if(this.packet_count === 0) this.packet_count = Number(this.segments_urls[0].split('index.m3u8/sq/')[1].split('/')[0]) - for await (let url of this.segments_urls){ - await (async () => { - return new Promise(async (resolve, reject) => { - if(Number(url.split('index.m3u8/sq/')[1].split('/')[0]) !== this.packet_count){ - resolve('') - return - } - let stream = this.got_stream(url) - stream.on('data', (chunk) => this.stream.write(chunk)) - stream.on('end', () => { - this.packet_count++ - resolve('') - }) - }) - })() - } - } - - private got_stream(url: string){ - return got.stream(url) } } @@ -285,6 +235,6 @@ export class Stream { this.timer = setTimeout(() => { this.loop() - }, 290 * 1000) + }, 300 * 1000) } } diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 00db863..f6ab7e5 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -9,10 +9,6 @@ export enum StreamType{ Opus = 'opus', } -interface StreamOptions { - low_latency : boolean; - preferred_quality : "144p" | "240p" | "360p" | "480p" | "720p" | "1080p" -} interface InfoData{ LiveStreamData : { @@ -38,14 +34,12 @@ function parseAudioFormats(formats : any[]){ return result } -export async function stream(url : string, options : StreamOptions = { low_latency : false, preferred_quality : "144p" }): Promise{ +export async function stream(url : string): Promise{ let info = await video_info(url) let final: any[] = []; let type : StreamType; - if(!options.low_latency) options.low_latency = false - if(!options.preferred_quality) options.preferred_quality = "144p" if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null) { - return await live_stream(info as InfoData, options) + return await live_stream(info as InfoData) } let audioFormat = parseAudioFormats(info.format) @@ -68,13 +62,11 @@ export async function stream(url : string, options : StreamOptions = { low_laten return new Stream(final[0].url, type, info.video_details.durationInSec) } -export async function stream_from_info(info : InfoData, options : StreamOptions = { low_latency : false, preferred_quality : "144p" }): Promise{ +export async function stream_from_info(info : InfoData): Promise{ let final: any[] = []; let type : StreamType; - if(!options.low_latency) options.low_latency = false - if(!options.preferred_quality) options.preferred_quality = "144p" if(info.LiveStreamData.isLive === true && info.LiveStreamData.hlsManifestUrl !== null) { - return await live_stream(info as InfoData, options) + return await live_stream(info as InfoData) } let audioFormat = parseAudioFormats(info.format) @@ -105,22 +97,18 @@ function filterFormat(formats : any[], codec : string){ return result } -async function live_stream(info : InfoData, options : StreamOptions): Promise{ +async function live_stream(info : InfoData): Promise{ let res_144 : FormatInterface = { url : '', targetDurationSec : 0, maxDvrDurationSec : 0 } - info.format.forEach((format) => { - if(format.qualityLabel === options.preferred_quality) res_144 = format - else return - }) let stream : LiveStreaming | LiveEnded - if(info.video_details.duration === '0') { - stream = new LiveStreaming((res_144.url.length !== 0) ? res_144 : info.format[info.format.length - 2], options.low_latency) + if(info.video_details.durationInSec === '0') { + stream = new LiveStreaming(info.LiveStreamData.dashManifestUrl, info.format[info.format.length - 1].targetDurationSec) } else { - stream = new LiveEnded((res_144.url.length !== 0) ? res_144 : info.format[info.format.length - 2]) + stream = new LiveEnded(info.format[info.format.length - 2]) } return stream } \ No newline at end of file diff --git a/play-dl/YouTube/utils/extractor.ts b/play-dl/YouTube/utils/extractor.ts index 5d8d446..d5fca69 100644 --- a/play-dl/YouTube/utils/extractor.ts +++ b/play-dl/YouTube/utils/extractor.ts @@ -75,8 +75,6 @@ function parseSeconds(seconds : number): string { export async function video_info(url : string) { let data = await video_basic_info(url) if(data.LiveStreamData.isLive === true && data.LiveStreamData.hlsManifestUrl !== null){ - let m3u8 = await url_get(data.LiveStreamData.hlsManifestUrl) - data.format = await parseM3U8(m3u8, data.format) return data } else if(data.format[0].signatureCipher || data.format[0].cipher){ @@ -88,25 +86,6 @@ export async function video_info(url : string) { } } -async function parseM3U8(m3u8_data : string, formats : any[]): Promise{ - let lines = m3u8_data.split('\n') - formats.forEach((format) => { - if(!format.qualityLabel) return - let reso = format.width + 'x' + format.height - let index = -1; - let line_count = 0 - lines.forEach((line) => { - index = line.search(reso) - if(index !== -1) { - format.url = lines[line_count+1] - } - line_count++ - index = -1 - }) - }) - return formats -} - export async function playlist_info(url : string, parseIncomplete : boolean = false) { if (!url || typeof url !== "string") throw new Error(`Expected playlist url, received ${typeof url}!`); if(url.search('(\\?|\\&)list\\=') === -1) throw new Error('This is not a PlayList URL')