Skip to content

Commit

Permalink
[YT seek] Only request header data and stop parsing header after reac…
Browse files Browse the repository at this point in the history
…hing the desired cue
  • Loading branch information
absidue committed Jan 6, 2022
1 parent e5707f0 commit ce45f3c
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 18 deletions.
34 changes: 26 additions & 8 deletions play-dl/YouTube/classes/SeekStream.ts
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
});
Expand All @@ -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(() => {
Expand All @@ -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<void> {
private async seek(): Promise<void> {
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);

Expand All @@ -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();
Expand All @@ -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;
Expand Down
31 changes: 21 additions & 10 deletions play-dl/YouTube/classes/WebmSeeker.ts
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -130,18 +135,24 @@ 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
);
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;
Expand Down
1 change: 1 addition & 0 deletions play-dl/YouTube/stream.ts
Expand Up @@ -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
Expand Down

0 comments on commit ce45f3c

Please sign in to comment.