import { WebmElements, WebmHeader } from 'play-audio' import { Duplex, DuplexOptions } from 'stream' enum DataType { master, string, uint, binary, float } export enum WebmSeekerState{ READING_HEAD = 'READING_HEAD', READING_DATA = 'READING_DATA', } export class WebmSeeker extends Duplex{ remaining? : Buffer state : WebmSeekerState chunk? : Buffer cursor : number header : WebmHeader headfound : boolean seekfound : boolean private data_size : number private data_length : number constructor(options? : DuplexOptions){ super(options) this.state = WebmSeekerState.READING_HEAD this.cursor = 0 this.header = new WebmHeader() this.headfound = false this.seekfound = false this.data_length = 0 this.data_size = 0 } private get vint_length(): number{ let i = 0; for (; i < 8; i++){ if ((1 << (7 - i)) & this.chunk![this.cursor]) break; } return ++i; } private get vint_value(): boolean { 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) for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i]; this.data_size = length this.data_length = value return true } cleanup(){ this.cursor = 0 this.chunk = undefined this.remaining = undefined } _read() {} seek(ms : number): Error | number{ let position = 0 let time = (Math.floor(ms / 10) * 10) if (!this.header.segment.cues) return new Error("Failed to Parse Cues") for(const data of this.header.segment.cues){ if(Math.floor(data.time as number / 1000) === time) { position = data.position as number break; } else continue; } if(position === 0) return Error("Failed to find Cluster Position") else return position } _write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void { if (this.remaining) { this.chunk = Buffer.concat([this.remaining, chunk]) this.remaining = undefined } else this.chunk = chunk let err : Error | undefined; if(this.state === WebmSeekerState.READING_HEAD) err = this.readHead() else if(!this.seekfound) err = this.getClosetCluster() else err = this.readTag() if(err) callback(err) else callback() } 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; const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex')) this.cursor += id const vint = this.vint_value if(!vint) { this.cursor = oldCursor break; } if(!ebmlID){ this.cursor += this.data_size + this.data_length continue; } if(!this.headfound){ if(ebmlID.name === "ebml") this.headfound = true else return new Error("Failed to find EBML ID at start of stream.") } 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 if(ebmlID.type === DataType.master) { this.cursor += this.data_size continue; } if(this.chunk.length < this.cursor + this.data_size + this.data_length) { this.cursor = oldCursor; break; } else this.cursor += this.data_size + this.data_length } this.remaining = this.chunk.slice(this.cursor) this.cursor = 0 } private readTag(): 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; const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex')) this.cursor += id const vint = this.vint_value if(!vint) { this.cursor = oldCursor break; } if(!ebmlID){ this.cursor += this.data_size + this.data_length continue; } 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 if(ebmlID.type === DataType.master) { this.cursor += this.data_size continue; } if(this.chunk.length < this.cursor + this.data_size + this.data_length) { this.cursor = oldCursor; break; } else this.cursor += this.data_size + this.data_length 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)) } } this.remaining = this.chunk.slice(this.cursor) this.cursor = 0 } private getClosetCluster(): Error | undefined{ if(!this.chunk) return new Error("Chunk is missing") const count = this.chunk.indexOf('1f43b675', 0, 'hex') if(count === -1) throw new Error("Failed to find nearest Cluster.") else this.chunk = this.chunk.slice(count) this.seekfound = true return this.readTag() } private parseEbmlID(ebmlID : string){ if(Object.keys(WebmElements).includes(ebmlID)) return WebmElements[ebmlID] else return false } _destroy(error : Error | null, callback : (error : Error | null) => void) : void { this.cleanup() callback(error); } _final(callback: (error?: Error | null) => void): void { this.cleanup(); callback(); } }