export class MediaSourceAppender { private readonly mediaSource = new MediaSource(); private readonly audioChunks: ArrayBuffer[] = []; private sourceBuffer?: SourceBuffer; constructor(type: string) { this.mediaSource.addEventListener('sourceopen', async () => { this.sourceBuffer = this.mediaSource.addSourceBuffer(type); this.sourceBuffer.addEventListener('updateend', () => { this.tryAppendNextChunk(); }); }); } private tryAppendNextChunk() { if (this.sourceBuffer != null && !this.sourceBuffer.updating && this.audioChunks.length > 0) { this.sourceBuffer.appendBuffer(this.audioChunks.shift()!); } } public addBase64Data(base64Data: string) { this.addData(Uint8Array.from(atob(base64Data), (char) => char.charCodeAt(0)).buffer); } public addData(data: ArrayBuffer) { this.audioChunks.push(data); this.tryAppendNextChunk(); } public close() { if (this.mediaSource.readyState === 'open') { this.mediaSource.endOfStream(); } } public get mediaSourceUrl() { return URL.createObjectURL(this.mediaSource); } }