Files
play-dl-test/play-dl/YouTube/classes/WebmSeeker.ts

264 lines
9.0 KiB
TypeScript
Raw Normal View History

2021-12-14 15:01:10 +05:30
import { WebmElements, WebmHeader } from 'play-audio';
2021-12-29 21:43:33 +01:00
import { Duplex, DuplexOptions } from 'node:stream';
2021-12-14 15:01:10 +05:30
enum DataType {
master,
string,
uint,
binary,
float
}
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
export enum WebmSeekerState {
2021-12-13 09:45:34 +05:30
READING_HEAD = 'READING_HEAD',
2021-12-14 15:01:10 +05:30
READING_DATA = 'READING_DATA'
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
interface WebmSeekerOptions extends DuplexOptions {
mode?: 'precise' | 'granular';
2021-12-13 12:30:54 +05:30
}
2021-12-30 17:24:38 +01:00
const WEB_ELEMENT_KEYS = Object.keys(WebmElements);
2021-12-14 15:01:10 +05:30
export class WebmSeeker extends Duplex {
remaining?: Buffer;
state: WebmSeekerState;
chunk?: Buffer;
cursor: number;
header: WebmHeader;
headfound: boolean;
headerparsed: boolean;
seekfound: boolean;
private data_size: number;
private offset: number;
2021-12-14 15:01:10 +05:30
private data_length: number;
private sec: number;
private time: number;
2021-12-14 15:01:10 +05:30
constructor(sec: number, options: WebmSeekerOptions) {
2021-12-14 15:01:10 +05:30
super(options);
this.state = WebmSeekerState.READING_HEAD;
this.cursor = 0;
this.header = new WebmHeader();
this.headfound = false;
this.headerparsed = false;
this.seekfound = false;
this.data_length = 0;
this.data_size = 0;
this.offset = 0
this.sec = sec;
this.time = Math.floor(sec / 10) * 10;
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
private get vint_length(): number {
2021-12-13 09:45:34 +05:30
let i = 0;
2021-12-14 15:01:10 +05:30
for (; i < 8; i++) {
if ((1 << (7 - i)) & this.chunk![this.cursor]) break;
}
return ++i;
2021-12-13 09:45:34 +05:30
}
2021-12-29 21:43:33 +01:00
private vint_value(): boolean {
2021-12-14 15:01:10 +05:30
if (!this.chunk) return false;
const length = this.vint_length;
if (this.chunk.length < this.cursor + length) return false;
let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1);
2021-12-13 09:45:34 +05:30
for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];
2021-12-14 15:01:10 +05:30
this.data_size = length;
this.data_length = value;
return true;
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
cleanup() {
this.cursor = 0;
this.chunk = undefined;
this.remaining = undefined;
2021-12-13 09:45:34 +05:30
}
_read() {}
2022-01-19 09:29:30 +05:30
seek(content_length: number): Error | number {
2022-01-05 14:21:31 +05:30
let clusterlength = 0,
position = 0;
let time_left = (this.sec - this.time) * 1000 || 0;
2022-01-05 12:05:07 +05:30
time_left = Math.round(time_left / 20) * 20;
2021-12-14 15:01:10 +05:30
if (!this.header.segment.cues) return new Error('Failed to Parse Cues');
2021-12-13 09:45:34 +05:30
2022-01-05 12:05:07 +05:30
for (let i = 0; i < this.header.segment.cues.length; i++) {
2022-01-05 14:21:31 +05:30
const data = this.header.segment.cues[i];
if (Math.floor((data.time as number) / 1000) === this.time) {
2021-12-14 15:01:10 +05:30
position = data.position as number;
2022-01-19 09:29:30 +05:30
clusterlength = (this.header.segment.cues[i + 1]?.position || content_length) - position - 1;
2021-12-13 09:45:34 +05:30
break;
2021-12-14 15:01:10 +05:30
} else continue;
2021-12-13 09:45:34 +05:30
}
if (clusterlength === 0) return position;
return this.offset + Math.round(position + (time_left / 20) * (clusterlength / 500));
2021-12-13 09:45:34 +05:30
}
_write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {
if (this.remaining) {
2021-12-14 15:01:10 +05:30
this.chunk = Buffer.concat([this.remaining, chunk]);
this.remaining = undefined;
} else this.chunk = chunk;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
let err: Error | undefined;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
if (this.state === WebmSeekerState.READING_HEAD) err = this.readHead();
2022-01-05 12:05:07 +05:30
else if (!this.seekfound) err = this.getClosestBlock();
2021-12-14 15:01:10 +05:30
else err = this.readTag();
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
if (err) callback(err);
else callback();
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
private readHead(): Error | undefined {
if (!this.chunk) return new Error('Chunk is missing');
while (this.chunk.length > this.cursor) {
const oldCursor = this.cursor;
const id = this.vint_length;
if (this.chunk.length < this.cursor + id) break;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));
this.cursor += id;
2021-12-13 09:45:34 +05:30
2021-12-29 21:43:33 +01:00
if (!this.vint_value()) {
2021-12-14 15:01:10 +05:30
this.cursor = oldCursor;
2021-12-13 09:45:34 +05:30
break;
}
2021-12-14 15:01:10 +05:30
if (!ebmlID) {
this.cursor += this.data_size + this.data_length;
2021-12-13 09:45:34 +05:30
continue;
}
2021-12-14 15:01:10 +05:30
if (!this.headfound) {
if (ebmlID.name === 'ebml') this.headfound = true;
else return new Error('Failed to find EBML ID at start of stream.');
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
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 === "seekHead") this.offset = oldCursor
2022-01-07 10:42:29 +05:30
if (
ebmlID.name === 'cueClusterPosition' &&
2022-01-19 09:29:30 +05:30
this.header.segment.cues!.length > 2 &&
this.time === (this.header.segment.cues!.at(-2)!.time as number) / 1000
2022-01-07 10:42:29 +05:30
)
this.emit('headComplete');
2021-12-14 15:01:10 +05:30
if (ebmlID.type === DataType.master) {
this.cursor += this.data_size;
2021-12-13 09:45:34 +05:30
continue;
}
2021-12-14 15:01:10 +05:30
if (this.chunk.length < this.cursor + this.data_size + this.data_length) {
2021-12-13 09:45:34 +05:30
this.cursor = oldCursor;
break;
2021-12-14 15:01:10 +05:30
} else this.cursor += this.data_size + this.data_length;
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
this.remaining = this.chunk.slice(this.cursor);
this.cursor = 0;
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
private readTag(): Error | undefined {
if (!this.chunk) return new Error('Chunk is missing');
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
while (this.chunk.length > this.cursor) {
const oldCursor = this.cursor;
const id = this.vint_length;
if (this.chunk.length < this.cursor + id) break;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'));
this.cursor += id;
2021-12-29 21:43:33 +01:00
if (!this.vint_value()) {
2021-12-14 15:01:10 +05:30
this.cursor = oldCursor;
2021-12-13 09:45:34 +05:30
break;
}
2021-12-14 15:01:10 +05:30
if (!ebmlID) {
this.cursor += this.data_size + this.data_length;
2021-12-13 09:45:34 +05:30
continue;
}
2021-12-14 15:01:10 +05:30
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;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
if (ebmlID.type === DataType.master) {
this.cursor += this.data_size;
2021-12-13 09:45:34 +05:30
continue;
}
2021-12-14 15:01:10 +05:30
if (this.chunk.length < this.cursor + this.data_size + this.data_length) {
2021-12-13 09:45:34 +05:30
this.cursor = oldCursor;
break;
2021-12-14 15:01:10 +05:30
} else this.cursor += this.data_size + this.data_length;
2021-12-13 09:45:34 +05:30
2021-12-14 15:01:10 +05:30
if (ebmlID.name === 'simpleBlock') {
const track = this.header.segment.tracks![this.header.audioTrack];
if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');
if ((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4));
2021-12-13 09:45:34 +05:30
}
}
2021-12-14 15:01:10 +05:30
this.remaining = this.chunk.slice(this.cursor);
this.cursor = 0;
2021-12-13 09:45:34 +05:30
}
2022-01-05 12:05:07 +05:30
private getClosestBlock(): Error | undefined {
if(this.sec === 0){
this.seekfound = true
return this.readTag()
}
2021-12-14 15:01:10 +05:30
if (!this.chunk) return new Error('Chunk is missing');
2022-01-05 14:21:31 +05:30
this.cursor = 0;
2022-01-05 12:05:07 +05:30
let positionFound = false;
2022-01-05 14:21:31 +05:30
while (!positionFound && this.cursor < this.chunk.length) {
2022-01-05 12:05:07 +05:30
this.cursor = this.chunk.indexOf('a3', this.cursor, 'hex');
if (this.cursor === -1) return new Error('Failed to find nearest Block.');
2022-01-05 14:21:31 +05:30
this.cursor++;
if (!this.vint_value()) return new Error('Failed to find correct simpleBlock in first chunk');
if (this.cursor + this.data_length + this.data_length > this.chunk.length) continue;
2022-01-05 12:05:07 +05:30
const data = this.chunk.slice(
this.cursor + this.data_size,
this.cursor + this.data_size + this.data_length
);
const track = this.header.segment.tracks![this.header.audioTrack];
if (!track || track.trackType !== 2) return new Error('No audio Track in this webm file.');
if ((data[0] & 0xf) === track.trackNumber) {
this.cursor += this.data_size + this.data_length;
2022-01-05 14:21:31 +05:30
this.push(data.slice(4));
2022-01-05 12:05:07 +05:30
positionFound = true;
2022-01-05 14:21:31 +05:30
} else continue;
2022-01-05 12:05:07 +05:30
}
2022-01-05 14:21:31 +05:30
if (!positionFound) return new Error('Failed to find nearest correct simple Block.');
2021-12-14 15:01:10 +05:30
this.seekfound = true;
return this.readTag();
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
private parseEbmlID(ebmlID: string) {
2021-12-30 17:24:38 +01:00
if (WEB_ELEMENT_KEYS.includes(ebmlID)) return WebmElements[ebmlID];
2021-12-14 15:01:10 +05:30
else return false;
2021-12-13 09:45:34 +05:30
}
2021-12-14 15:01:10 +05:30
_destroy(error: Error | null, callback: (error: Error | null) => void): void {
this.cleanup();
2021-12-13 09:45:34 +05:30
callback(error);
}
_final(callback: (error?: Error | null) => void): void {
this.cleanup();
callback();
}
2021-12-14 15:01:10 +05:30
}