OggDemuxer.js (3446B)
1 const { Transform } = require('stream'); 2 3 const OGG_PAGE_HEADER_SIZE = 26; 4 const STREAM_STRUCTURE_VERSION = 0; 5 6 const charCode = x => x.charCodeAt(0); 7 const OGGS_HEADER = Buffer.from([...'OggS'].map(charCode)); 8 const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode)); 9 const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode)); 10 11 /** 12 * Demuxes an Ogg stream (containing Opus audio) to output an Opus stream. 13 * @extends {TransformStream} 14 * @memberof opus 15 */ 16 class OggDemuxer extends Transform { 17 /** 18 * Creates a new OggOpus demuxer. 19 * @param {Object} [options] options that you would pass to a regular Transform stream. 20 * @memberof opus 21 */ 22 constructor(options = {}) { 23 super(Object.assign({ readableObjectMode: true }, options)); 24 this._remainder = null; 25 this._head = null; 26 this._bitstream = null; 27 } 28 29 _transform(chunk, encoding, done) { 30 if (this._remainder) { 31 chunk = Buffer.concat([this._remainder, chunk]); 32 this._remainder = null; 33 } 34 35 while (chunk) { 36 const result = this._readPage(chunk); 37 if (result) chunk = result; 38 else break; 39 } 40 this._remainder = chunk; 41 done(); 42 } 43 44 /** 45 * Reads a page from a buffer 46 * @private 47 * @param {Buffer} chunk the chunk containing the page 48 * @returns {boolean|Buffer} if a buffer, it will be a slice of the excess data of the original, otherwise it will be 49 * false and would indicate that there is not enough data to go ahead with reading this page. 50 */ 51 _readPage(chunk) { 52 if (chunk.length < OGG_PAGE_HEADER_SIZE) { 53 return false; 54 } 55 if (!chunk.slice(0, 4).equals(OGGS_HEADER)) { 56 throw Error(`capture_pattern is not ${OGGS_HEADER}`); 57 } 58 if (chunk.readUInt8(4) !== STREAM_STRUCTURE_VERSION) { 59 throw Error(`stream_structure_version is not ${STREAM_STRUCTURE_VERSION}`); 60 } 61 62 if (chunk.length < 27) return false; 63 const pageSegments = chunk.readUInt8(26); 64 if (chunk.length < 27 + pageSegments) return false; 65 const table = chunk.slice(27, 27 + pageSegments); 66 const bitstream = chunk.readUInt32BE(14); 67 68 let sizes = [], totalSize = 0; 69 70 for (let i = 0; i < pageSegments;) { 71 let size = 0, x = 255; 72 while (x === 255) { 73 if (i >= table.length) return false; 74 x = table.readUInt8(i); 75 i++; 76 size += x; 77 } 78 sizes.push(size); 79 totalSize += size; 80 } 81 82 if (chunk.length < 27 + pageSegments + totalSize) return false; 83 84 let start = 27 + pageSegments; 85 for (const size of sizes) { 86 const segment = chunk.slice(start, start + size); 87 const header = segment.slice(0, 8); 88 if (this._head) { 89 if (header.equals(OPUS_TAGS)) this.emit('tags', segment); 90 else if (this._bitstream === bitstream) this.push(segment); 91 } else if (header.equals(OPUS_HEAD)) { 92 this.emit('head', segment); 93 this._head = segment; 94 this._bitstream = bitstream; 95 } else { 96 this.emit('unknownSegment', segment); 97 } 98 start += size; 99 } 100 return chunk.slice(start); 101 } 102 } 103 104 /** 105 * Emitted when the demuxer encounters the opus head. 106 * @event OggDemuxer#head 107 * @memberof opus 108 * @param {Buffer} segment a buffer containing the opus head data. 109 */ 110 111 /** 112 * Emitted when the demuxer encounters opus tags. 113 * @event OggDemuxer#tags 114 * @memberof opus 115 * @param {Buffer} segment a buffer containing the opus tags. 116 */ 117 118 module.exports = OggDemuxer;