From ce45f3ca6251d615f57ed47428375141215e0fe1 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Wed, 5 Jan 2022 18:48:42 +0100 Subject: [PATCH] [YT seek] Only request header data and stop parsing header after reaching the desired cue --- play-dl/YouTube/classes/SeekStream.ts | 34 ++++++++++++++++++++------- play-dl/YouTube/classes/WebmSeeker.ts | 31 ++++++++++++++++-------- play-dl/YouTube/stream.ts | 1 + 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/play-dl/YouTube/classes/SeekStream.ts b/play-dl/YouTube/classes/SeekStream.ts index 3e6b8f9..3785d0f 100644 --- a/play-dl/YouTube/classes/SeekStream.ts +++ b/play-dl/YouTube/classes/SeekStream.ts @@ -29,6 +29,10 @@ export class SeekStream { * Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds) */ private per_sec_bytes: number; + /** + * Length of the header in bytes + */ + private header_length: number; /** * Total length of audio file in bytes */ @@ -57,12 +61,20 @@ export class SeekStream { * @param url Audio Endpoint url. * @param type Type of Stream * @param duration Duration of audio playback [ in seconds ] + * @param headerLength Length of the header in bytes. * @param contentLength Total length of Audio file in bytes. * @param video_url YouTube video url. * @param options Options provided to stream function. */ - constructor(url: string, duration: number, contentLength: number, video_url: string, options: StreamOptions) { - this.stream = new WebmSeeker({ + constructor( + url: string, + duration: number, + headerLength: number, + contentLength: number, + video_url: string, + options: StreamOptions + ) { + this.stream = new WebmSeeker(options.seek!, { highWaterMark: 5 * 1000 * 1000, readableObjectMode: true }); @@ -72,6 +84,7 @@ export class SeekStream { this.bytes_count = 0; this.video_url = video_url; this.per_sec_bytes = Math.ceil(contentLength / duration); + this.header_length = headerLength; this.content_length = contentLength; this.request = null; this.timer = new Timer(() => { @@ -82,21 +95,20 @@ export class SeekStream { this.timer.destroy(); this.cleanup(); }); - this.seek(options.seek!); + this.seek(); } /** * **INTERNAL Function** * * Uses stream functions to parse Webm Head and gets Offset byte to seek to. - * @param sec No of seconds to seek to * @returns Nothing */ - private async seek(sec: number): Promise { + private async seek(): Promise { const parse = await new Promise(async (res, rej) => { if (!this.stream.headerparsed) { const stream = await request_stream(this.url, { headers: { - range: `bytes=0-` + range: `bytes=0-${this.header_length}` } }).catch((err: Error) => err); @@ -111,6 +123,12 @@ export class SeekStream { this.request = stream; stream.pipe(this.stream, { end: false }); + // headComplete should always be called, leaving this here just in case + stream.once('end', () => { + this.stream.state = WebmSeekerState.READING_DATA; + res(''); + }); + this.stream.once('headComplete', () => { stream.unpipe(this.stream); stream.destroy(); @@ -128,9 +146,9 @@ export class SeekStream { } else if (parse === 400) { await this.retry(); this.timer.reuse(); - return this.seek(sec); + return this.seek(); } - const bytes = this.stream.seek(sec); + const bytes = this.stream.seek(); if (bytes instanceof Error) { this.stream.emit('error', bytes); this.bytes_count = 0; diff --git a/play-dl/YouTube/classes/WebmSeeker.ts b/play-dl/YouTube/classes/WebmSeeker.ts index c7b7256..f064f45 100644 --- a/play-dl/YouTube/classes/WebmSeeker.ts +++ b/play-dl/YouTube/classes/WebmSeeker.ts @@ -31,8 +31,11 @@ export class WebmSeeker extends Duplex { seekfound: boolean; private data_size: number; private data_length: number; + private sec: number; + private time: number; + private foundCue: boolean; - constructor(options: WebmSeekerOptions) { + constructor(sec: number, options: WebmSeekerOptions) { super(options); this.state = WebmSeekerState.READING_HEAD; this.cursor = 0; @@ -42,6 +45,9 @@ export class WebmSeeker extends Duplex { this.seekfound = false; this.data_length = 0; this.data_size = 0; + this.sec = sec; + this.time = Math.floor(sec / 10) * 10; + this.foundCue = false; } private get vint_length(): number { @@ -71,17 +77,16 @@ export class WebmSeeker extends Duplex { _read() {} - seek(sec: number): Error | number { + seek(): Error | number { let clusterlength = 0, position = 0; - const time = Math.floor(sec / 10) * 10; - let time_left = (sec - time) * 1000 || 0; + let time_left = (this.sec - this.time) * 1000 || 0; time_left = Math.round(time_left / 20) * 20; if (!this.header.segment.cues) return new Error('Failed to Parse Cues'); for (let i = 0; i < this.header.segment.cues.length; i++) { const data = this.header.segment.cues[i]; - if (Math.floor((data.time as number) / 1000) === time) { + if (Math.floor((data.time as number) / 1000) === this.time) { position = data.position as number; clusterlength = this.header.segment.cues[i + 1].position! - position - 1; break; @@ -130,11 +135,6 @@ export class WebmSeeker extends Duplex { if (ebmlID.name === 'ebml') this.headfound = true; else return new Error('Failed to find EBML ID at start of stream.'); } - if (ebmlID.name === 'cluster') { - this.emit('headComplete'); - this.cursor = this.chunk.length; - break; - } const data = this.chunk.slice( this.cursor + this.data_size, this.cursor + this.data_size + this.data_length @@ -142,6 +142,17 @@ export class WebmSeeker extends Duplex { const parse = this.header.parse(ebmlID, data); if (parse instanceof Error) return parse; + // stop parsing the header once we have found the correct cue + if (ebmlID.name === 'cueClusterPosition') { + if (this.foundCue) { + this.emit('headComplete'); + this.cursor = this.chunk.length; + break; + } else if (this.time === (this.header.segment.cues!.at(-1)!.time as number) / 1000) { + this.foundCue = true; + } + } + if (ebmlID.type === DataType.master) { this.cursor += this.data_size; continue; diff --git a/play-dl/YouTube/stream.ts b/play-dl/YouTube/stream.ts index 9d19cf9..52a9621 100644 --- a/play-dl/YouTube/stream.ts +++ b/play-dl/YouTube/stream.ts @@ -92,6 +92,7 @@ export async function stream_from_info( return new SeekStream( final[0].url, info.video_details.durationInSec, + final[0].indexRange.end, Number(final[0].contentLength), info.video_details.url, options