[YT seek] Only request header data and stop parsing header after reaching the desired cue
This commit is contained in:
parent
e5707f049b
commit
ce45f3ca62
@ -29,6 +29,10 @@ export class SeekStream {
|
|||||||
* Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)
|
* Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)
|
||||||
*/
|
*/
|
||||||
private per_sec_bytes: number;
|
private per_sec_bytes: number;
|
||||||
|
/**
|
||||||
|
* Length of the header in bytes
|
||||||
|
*/
|
||||||
|
private header_length: number;
|
||||||
/**
|
/**
|
||||||
* Total length of audio file in bytes
|
* Total length of audio file in bytes
|
||||||
*/
|
*/
|
||||||
@ -57,12 +61,20 @@ export class SeekStream {
|
|||||||
* @param url Audio Endpoint url.
|
* @param url Audio Endpoint url.
|
||||||
* @param type Type of Stream
|
* @param type Type of Stream
|
||||||
* @param duration Duration of audio playback [ in seconds ]
|
* @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 contentLength Total length of Audio file in bytes.
|
||||||
* @param video_url YouTube video url.
|
* @param video_url YouTube video url.
|
||||||
* @param options Options provided to stream function.
|
* @param options Options provided to stream function.
|
||||||
*/
|
*/
|
||||||
constructor(url: string, duration: number, contentLength: number, video_url: string, options: StreamOptions) {
|
constructor(
|
||||||
this.stream = new WebmSeeker({
|
url: string,
|
||||||
|
duration: number,
|
||||||
|
headerLength: number,
|
||||||
|
contentLength: number,
|
||||||
|
video_url: string,
|
||||||
|
options: StreamOptions
|
||||||
|
) {
|
||||||
|
this.stream = new WebmSeeker(options.seek!, {
|
||||||
highWaterMark: 5 * 1000 * 1000,
|
highWaterMark: 5 * 1000 * 1000,
|
||||||
readableObjectMode: true
|
readableObjectMode: true
|
||||||
});
|
});
|
||||||
@ -72,6 +84,7 @@ export class SeekStream {
|
|||||||
this.bytes_count = 0;
|
this.bytes_count = 0;
|
||||||
this.video_url = video_url;
|
this.video_url = video_url;
|
||||||
this.per_sec_bytes = Math.ceil(contentLength / duration);
|
this.per_sec_bytes = Math.ceil(contentLength / duration);
|
||||||
|
this.header_length = headerLength;
|
||||||
this.content_length = contentLength;
|
this.content_length = contentLength;
|
||||||
this.request = null;
|
this.request = null;
|
||||||
this.timer = new Timer(() => {
|
this.timer = new Timer(() => {
|
||||||
@ -82,21 +95,20 @@ export class SeekStream {
|
|||||||
this.timer.destroy();
|
this.timer.destroy();
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
});
|
});
|
||||||
this.seek(options.seek!);
|
this.seek();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* **INTERNAL Function**
|
* **INTERNAL Function**
|
||||||
*
|
*
|
||||||
* Uses stream functions to parse Webm Head and gets Offset byte to seek to.
|
* Uses stream functions to parse Webm Head and gets Offset byte to seek to.
|
||||||
* @param sec No of seconds to seek to
|
|
||||||
* @returns Nothing
|
* @returns Nothing
|
||||||
*/
|
*/
|
||||||
private async seek(sec: number): Promise<void> {
|
private async seek(): Promise<void> {
|
||||||
const parse = await new Promise(async (res, rej) => {
|
const parse = await new Promise(async (res, rej) => {
|
||||||
if (!this.stream.headerparsed) {
|
if (!this.stream.headerparsed) {
|
||||||
const stream = await request_stream(this.url, {
|
const stream = await request_stream(this.url, {
|
||||||
headers: {
|
headers: {
|
||||||
range: `bytes=0-`
|
range: `bytes=0-${this.header_length}`
|
||||||
}
|
}
|
||||||
}).catch((err: Error) => err);
|
}).catch((err: Error) => err);
|
||||||
|
|
||||||
@ -111,6 +123,12 @@ export class SeekStream {
|
|||||||
this.request = stream;
|
this.request = stream;
|
||||||
stream.pipe(this.stream, { end: false });
|
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', () => {
|
this.stream.once('headComplete', () => {
|
||||||
stream.unpipe(this.stream);
|
stream.unpipe(this.stream);
|
||||||
stream.destroy();
|
stream.destroy();
|
||||||
@ -128,9 +146,9 @@ export class SeekStream {
|
|||||||
} else if (parse === 400) {
|
} else if (parse === 400) {
|
||||||
await this.retry();
|
await this.retry();
|
||||||
this.timer.reuse();
|
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) {
|
if (bytes instanceof Error) {
|
||||||
this.stream.emit('error', bytes);
|
this.stream.emit('error', bytes);
|
||||||
this.bytes_count = 0;
|
this.bytes_count = 0;
|
||||||
|
|||||||
@ -31,8 +31,11 @@ export class WebmSeeker extends Duplex {
|
|||||||
seekfound: boolean;
|
seekfound: boolean;
|
||||||
private data_size: number;
|
private data_size: number;
|
||||||
private data_length: number;
|
private data_length: number;
|
||||||
|
private sec: number;
|
||||||
|
private time: number;
|
||||||
|
private foundCue: boolean;
|
||||||
|
|
||||||
constructor(options: WebmSeekerOptions) {
|
constructor(sec: number, options: WebmSeekerOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.state = WebmSeekerState.READING_HEAD;
|
this.state = WebmSeekerState.READING_HEAD;
|
||||||
this.cursor = 0;
|
this.cursor = 0;
|
||||||
@ -42,6 +45,9 @@ export class WebmSeeker extends Duplex {
|
|||||||
this.seekfound = false;
|
this.seekfound = false;
|
||||||
this.data_length = 0;
|
this.data_length = 0;
|
||||||
this.data_size = 0;
|
this.data_size = 0;
|
||||||
|
this.sec = sec;
|
||||||
|
this.time = Math.floor(sec / 10) * 10;
|
||||||
|
this.foundCue = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get vint_length(): number {
|
private get vint_length(): number {
|
||||||
@ -71,17 +77,16 @@ export class WebmSeeker extends Duplex {
|
|||||||
|
|
||||||
_read() {}
|
_read() {}
|
||||||
|
|
||||||
seek(sec: number): Error | number {
|
seek(): Error | number {
|
||||||
let clusterlength = 0,
|
let clusterlength = 0,
|
||||||
position = 0;
|
position = 0;
|
||||||
const time = Math.floor(sec / 10) * 10;
|
let time_left = (this.sec - this.time) * 1000 || 0;
|
||||||
let time_left = (sec - time) * 1000 || 0;
|
|
||||||
time_left = Math.round(time_left / 20) * 20;
|
time_left = Math.round(time_left / 20) * 20;
|
||||||
if (!this.header.segment.cues) return new Error('Failed to Parse Cues');
|
if (!this.header.segment.cues) return new Error('Failed to Parse Cues');
|
||||||
|
|
||||||
for (let i = 0; i < this.header.segment.cues.length; i++) {
|
for (let i = 0; i < this.header.segment.cues.length; i++) {
|
||||||
const data = this.header.segment.cues[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;
|
position = data.position as number;
|
||||||
clusterlength = this.header.segment.cues[i + 1].position! - position - 1;
|
clusterlength = this.header.segment.cues[i + 1].position! - position - 1;
|
||||||
break;
|
break;
|
||||||
@ -130,11 +135,6 @@ export class WebmSeeker extends Duplex {
|
|||||||
if (ebmlID.name === 'ebml') this.headfound = true;
|
if (ebmlID.name === 'ebml') this.headfound = true;
|
||||||
else return new Error('Failed to find EBML ID at start of stream.');
|
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(
|
const data = this.chunk.slice(
|
||||||
this.cursor + this.data_size,
|
this.cursor + this.data_size,
|
||||||
this.cursor + this.data_size + this.data_length
|
this.cursor + this.data_size + this.data_length
|
||||||
@ -142,6 +142,17 @@ export class WebmSeeker extends Duplex {
|
|||||||
const parse = this.header.parse(ebmlID, data);
|
const parse = this.header.parse(ebmlID, data);
|
||||||
if (parse instanceof Error) return parse;
|
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) {
|
if (ebmlID.type === DataType.master) {
|
||||||
this.cursor += this.data_size;
|
this.cursor += this.data_size;
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -92,6 +92,7 @@ export async function stream_from_info(
|
|||||||
return new SeekStream(
|
return new SeekStream(
|
||||||
final[0].url,
|
final[0].url,
|
||||||
info.video_details.durationInSec,
|
info.video_details.durationInSec,
|
||||||
|
final[0].indexRange.end,
|
||||||
Number(final[0].contentLength),
|
Number(final[0].contentLength),
|
||||||
info.video_details.url,
|
info.video_details.url,
|
||||||
options
|
options
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user