2021-12-13 09:45:34 +05:30

207 lines
6.6 KiB
TypeScript

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();
}
}