WebmBase.js (6617B)
1 const { Transform } = require('stream'); 2 3 /** 4 * Base class for WebmOpusDemuxer and WebmVorbisDemuxer. 5 * **You shouldn't directly instantiate this class, use the opus.WebmDemuxer and vorbis.WebmDemuxer 6 * implementations instead!** 7 * @memberof core 8 * @protected 9 * @extends TransformStream 10 */ 11 class WebmBaseDemuxer extends Transform { 12 /** 13 * Creates a new Webm demuxer. 14 * @private 15 * @memberof core 16 * @param {Object} [options] options that you would pass to a regular Transform stream. 17 */ 18 constructor(options = {}) { 19 super(Object.assign({ readableObjectMode: true }, options)); 20 this._remainder = null; 21 this._length = 0; 22 this._count = 0; 23 this._skipUntil = null; 24 this._track = null; 25 this._incompleteTrack = {}; 26 this._ebmlFound = false; 27 } 28 29 _transform(chunk, encoding, done) { 30 this._length += chunk.length; 31 if (this._remainder) { 32 chunk = Buffer.concat([this._remainder, chunk]); 33 this._remainder = null; 34 } 35 let offset = 0; 36 if (this._skipUntil && this._length > this._skipUntil) { 37 offset = this._skipUntil - this._count; 38 this._skipUntil = null; 39 } else if (this._skipUntil) { 40 this._count += chunk.length; 41 return done(); 42 } 43 let result; 44 while (result !== TOO_SHORT) { 45 result = this._readTag(chunk, offset); 46 if (result === TOO_SHORT) break; 47 if (result._skipUntil) { 48 this._skipUntil = result._skipUntil; 49 break; 50 } 51 if (result.offset) offset = result.offset; 52 else break; 53 } 54 this._count += offset; 55 this._remainder = chunk.slice(offset); 56 return done(); 57 } 58 59 /** 60 * Reads an EBML ID from a buffer. 61 * @private 62 * @param {Buffer} chunk the buffer to read from. 63 * @param {number} offset the offset in the buffer. 64 * @returns {Object|Symbol} contains an `id` property (buffer) and the new `offset` (number). 65 * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. 66 */ 67 _readEBMLId(chunk, offset) { 68 const idLength = vintLength(chunk, offset); 69 if (idLength === TOO_SHORT) return TOO_SHORT; 70 return { 71 id: chunk.slice(offset, offset + idLength), 72 offset: offset + idLength, 73 }; 74 } 75 76 /** 77 * Reads a size variable-integer to calculate the length of the data of a tag. 78 * @private 79 * @param {Buffer} chunk the buffer to read from. 80 * @param {number} offset the offset in the buffer. 81 * @returns {Object|Symbol} contains property `offset` (number), `dataLength` (number) and `sizeLength` (number). 82 * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. 83 */ 84 _readTagDataSize(chunk, offset) { 85 const sizeLength = vintLength(chunk, offset); 86 if (sizeLength === TOO_SHORT) return TOO_SHORT; 87 const dataLength = expandVint(chunk, offset, offset + sizeLength); 88 return { offset: offset + sizeLength, dataLength, sizeLength }; 89 } 90 91 /** 92 * Takes a buffer and attempts to read and process a tag. 93 * @private 94 * @param {Buffer} chunk the buffer to read from. 95 * @param {number} offset the offset in the buffer. 96 * @returns {Object|Symbol} contains the new `offset` (number) and optionally the `_skipUntil` property, 97 * indicating that the stream should ignore any data until a certain length is reached. 98 * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. 99 */ 100 _readTag(chunk, offset) { 101 const idData = this._readEBMLId(chunk, offset); 102 if (idData === TOO_SHORT) return TOO_SHORT; 103 const ebmlID = idData.id.toString('hex'); 104 if (!this._ebmlFound) { 105 if (ebmlID === '1a45dfa3') this._ebmlFound = true; 106 else throw Error('Did not find the EBML tag at the start of the stream'); 107 } 108 offset = idData.offset; 109 const sizeData = this._readTagDataSize(chunk, offset); 110 if (sizeData === TOO_SHORT) return TOO_SHORT; 111 const { dataLength } = sizeData; 112 offset = sizeData.offset; 113 // If this tag isn't useful, tell the stream to stop processing data until the tag ends 114 if (typeof TAGS[ebmlID] === 'undefined') { 115 if (chunk.length > offset + dataLength) { 116 return { offset: offset + dataLength }; 117 } 118 return { offset, _skipUntil: this._count + offset + dataLength }; 119 } 120 121 const tagHasChildren = TAGS[ebmlID]; 122 if (tagHasChildren) { 123 return { offset }; 124 } 125 126 if (offset + dataLength > chunk.length) return TOO_SHORT; 127 const data = chunk.slice(offset, offset + dataLength); 128 if (!this._track) { 129 if (ebmlID === 'ae') this._incompleteTrack = {}; 130 if (ebmlID === 'd7') this._incompleteTrack.number = data[0]; 131 if (ebmlID === '83') this._incompleteTrack.type = data[0]; 132 if (this._incompleteTrack.type === 2 && typeof this._incompleteTrack.number !== 'undefined') { 133 this._track = this._incompleteTrack; 134 } 135 } 136 if (ebmlID === '63a2') { 137 this._checkHead(data); 138 } else if (ebmlID === 'a3') { 139 if (!this._track) throw Error('No audio track in this webm!'); 140 if ((data[0] & 0xF) === this._track.number) { 141 this.push(data.slice(4)); 142 } 143 } 144 return { offset: offset + dataLength }; 145 } 146 } 147 148 /** 149 * A symbol that is returned by some functions that indicates the buffer it has been provided is not large enough 150 * to facilitate a request. 151 * @name WebmBaseDemuxer#TOO_SHORT 152 * @memberof core 153 * @private 154 * @type {Symbol} 155 */ 156 const TOO_SHORT = WebmBaseDemuxer.TOO_SHORT = Symbol('TOO_SHORT'); 157 158 /** 159 * A map that takes a value of an EBML ID in hex string form, with the value being a boolean that indicates whether 160 * this tag has children. 161 * @name WebmBaseDemuxer#TAGS 162 * @memberof core 163 * @private 164 * @type {Object} 165 */ 166 const TAGS = WebmBaseDemuxer.TAGS = { // value is true if the element has children 167 '1a45dfa3': true, // EBML 168 '18538067': true, // Segment 169 '1f43b675': true, // Cluster 170 '1654ae6b': true, // Tracks 171 'ae': true, // TrackEntry 172 'd7': false, // TrackNumber 173 '83': false, // TrackType 174 'a3': false, // SimpleBlock 175 '63a2': false, 176 }; 177 178 module.exports = WebmBaseDemuxer; 179 180 function vintLength(buffer, index) { 181 let i = 0; 182 for (; i < 8; i++) if ((1 << (7 - i)) & buffer[index]) break; 183 i++; 184 if (index + i > buffer.length) { 185 return TOO_SHORT; 186 } 187 return i; 188 } 189 190 function expandVint(buffer, start, end) { 191 const length = vintLength(buffer, start); 192 if (end > buffer.length || length === TOO_SHORT) return TOO_SHORT; 193 let mask = (1 << (8 - length)) - 1; 194 let value = buffer[start] & mask; 195 for (let i = start + 1; i < end; i++) { 196 value = (value << 8) + buffer[i]; 197 } 198 return value; 199 }