const { Transform } = require('stream'); const OGG_PAGE_HEADER_SIZE = 26; const STREAM_STRUCTURE_VERSION = 0; const charCode = x => x.charCodeAt(0); const OGGS_HEADER = Buffer.from([...'OggS'].map(charCode)); const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode)); const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode)); /** * Demuxes an Ogg stream (containing Opus audio) to output an Opus stream. * @extends {TransformStream} * @memberof opus */ class OggDemuxer extends Transform { /** * Creates a new OggOpus demuxer. * @param {Object} [options] options that you would pass to a regular Transform stream. * @memberof opus */ constructor(options = {}) { super(Object.assign({ readableObjectMode: true }, options)); this._remainder = null; this._head = null; this._bitstream = null; } _transform(chunk, encoding, done) { if (this._remainder) { chunk = Buffer.concat([this._remainder, chunk]); this._remainder = null; } while (chunk) { const result = this._readPage(chunk); if (result) chunk = result; else break; } this._remainder = chunk; done(); } /** * Reads a page from a buffer * @private * @param {Buffer} chunk the chunk containing the page * @returns {boolean|Buffer} if a buffer, it will be a slice of the excess data of the original, otherwise it will be * false and would indicate that there is not enough data to go ahead with reading this page. */ _readPage(chunk) { if (chunk.length < OGG_PAGE_HEADER_SIZE) { return false; } if (!chunk.slice(0, 4).equals(OGGS_HEADER)) { throw Error(`capture_pattern is not ${OGGS_HEADER}`); } if (chunk.readUInt8(4) !== STREAM_STRUCTURE_VERSION) { throw Error(`stream_structure_version is not ${STREAM_STRUCTURE_VERSION}`); } if (chunk.length < 27) return false; const pageSegments = chunk.readUInt8(26); if (chunk.length < 27 + pageSegments) return false; const table = chunk.slice(27, 27 + pageSegments); const bitstream = chunk.readUInt32BE(14); let sizes = [], totalSize = 0; for (let i = 0; i < pageSegments;) { let size = 0, x = 255; while (x === 255) { if (i >= table.length) return false; x = table.readUInt8(i); i++; size += x; } sizes.push(size); totalSize += size; } if (chunk.length < 27 + pageSegments + totalSize) return false; let start = 27 + pageSegments; for (const size of sizes) { const segment = chunk.slice(start, start + size); const header = segment.slice(0, 8); if (this._head) { if (header.equals(OPUS_TAGS)) this.emit('tags', segment); else if (this._bitstream === bitstream) this.push(segment); } else if (header.equals(OPUS_HEAD)) { this.emit('head', segment); this._head = segment; this._bitstream = bitstream; } else { this.emit('unknownSegment', segment); } start += size; } return chunk.slice(start); } } /** * Emitted when the demuxer encounters the opus head. * @event OggDemuxer#head * @memberof opus * @param {Buffer} segment a buffer containing the opus head data. */ /** * Emitted when the demuxer encounters opus tags. * @event OggDemuxer#tags * @memberof opus * @param {Buffer} segment a buffer containing the opus tags. */ module.exports = OggDemuxer;