All files / src/demux id3.js

0.85% Statements 1/118
0% Branches 0/88
0% Functions 0/14
0.85% Lines 1/118

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        1x          
/**
 * ID3 parser
 */
class ID3 {
  /**
   * Returns true if an ID3 header can be found at offset in data
   * @param {Uint8Array} data - The data to search in
   * @param {number} offset - The offset at which to start searching
   * @return {boolean} - True if an ID3 header is found
   */
  static isHeader (data, offset) {
    /*
    * http://id3.org/id3v2.3.0
    * [0]     = 'I'
    * [1]     = 'D'
    * [2]     = '3'
    * [3,4]   = {Version}
    * [5]     = {Flags}
    * [6-9]   = {ID3 Size}
    *
    * An ID3v2 tag can be detected with the following pattern:
    *  $49 44 33 yy yy xx zz zz zz zz
    * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
    */
    if (offset + 10 <= data.length) {
      // look for 'ID3' identifier
      if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) {
        // check version is within range
        if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
          // check size is within range
          if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80)
            return true;
        }
      }
    }
 
    return false;
  }
 
  /**
   * Returns true if an ID3 footer can be found at offset in data
   * @param {Uint8Array} data - The data to search in
   * @param {number} offset - The offset at which to start searching
   * @return {boolean} - True if an ID3 footer is found
   */
  static isFooter (data, offset) {
    /*
    * The footer is a copy of the header, but with a different identifier
    */
    if (offset + 10 <= data.length) {
      // look for '3DI' identifier
      if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) {
        // check version is within range
        if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
          // check size is within range
          if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80)
            return true;
        }
      }
    }
 
    return false;
  }
 
  /**
   * Returns any adjacent ID3 tags found in data starting at offset, as one block of data
   * @param {Uint8Array} data - The data to search in
   * @param {number} offset - The offset at which to start searching
   * @return {Uint8Array} - The block of data containing any ID3 tags found
   */
  static getID3Data (data, offset) {
    const front = offset;
    let length = 0;
 
    while (ID3.isHeader(data, offset)) {
      // ID3 header is 10 bytes
      length += 10;
 
      const size = ID3._readSize(data, offset + 6);
      length += size;
 
      if (ID3.isFooter(data, offset + 10)) {
        // ID3 footer is 10 bytes
        length += 10;
      }
 
      offset += length;
    }
 
    if (length > 0)
      return data.subarray(front, front + length);
 
    return undefined;
  }
 
  static _readSize (data, offset) {
    let size = 0;
    size = ((data[offset] & 0x7f) << 21);
    size |= ((data[offset + 1] & 0x7f) << 14);
    size |= ((data[offset + 2] & 0x7f) << 7);
    size |= (data[offset + 3] & 0x7f);
    return size;
  }
 
  /**
   * Searches for the Elementary Stream timestamp found in the ID3 data chunk
   * @param {Uint8Array} data - Block of data containing one or more ID3 tags
   * @return {number} - The timestamp
   */
  static getTimeStamp (data) {
    const frames = ID3.getID3Frames(data);
    for (let i = 0; i < frames.length; i++) {
      const frame = frames[i];
      if (ID3.isTimeStampFrame(frame))
        return ID3._readTimeStamp(frame);
    }
 
    return undefined;
  }
 
  /**
   * Returns true if the ID3 frame is an Elementary Stream timestamp frame
   * @param {ID3 frame} frame
   */
  static isTimeStampFrame (frame) {
    return (frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp');
  }
 
  static _getFrameData (data) {
    /*
    Frame ID       $xx xx xx xx (four characters)
    Size           $xx xx xx xx
    Flags          $xx xx
    */
    const type = String.fromCharCode(data[0], data[1], data[2], data[3]);
    const size = ID3._readSize(data, 4);
 
    // skip frame id, size, and flags
    let offset = 10;
 
    return { type, size, data: data.subarray(offset, offset + size) };
  }
 
  /**
   * Returns an array of ID3 frames found in all the ID3 tags in the id3Data
   * @param {Uint8Array} id3Data - The ID3 data containing one or more ID3 tags
   * @return {ID3 frame[]} - Array of ID3 frame objects
   */
  static getID3Frames (id3Data) {
    let offset = 0;
    const frames = [];
 
    while (ID3.isHeader(id3Data, offset)) {
      const size = ID3._readSize(id3Data, offset + 6);
      // skip past ID3 header
      offset += 10;
      const end = offset + size;
      // loop through frames in the ID3 tag
      while (offset + 8 < end) {
        const frameData = ID3._getFrameData(id3Data.subarray(offset));
        const frame = ID3._decodeFrame(frameData);
        if (frame)
          frames.push(frame);
 
        // skip frame header and frame data
        offset += frameData.size + 10;
      }
 
      if (ID3.isFooter(id3Data, offset))
        offset += 10;
    }
 
    return frames;
  }
 
  static _decodeFrame (frame) {
    if (frame.type === 'PRIV')
      return ID3._decodePrivFrame(frame);
    else if (frame.type[0] === 'T')
      return ID3._decodeTextFrame(frame);
    else if (frame.type[0] === 'W')
      return ID3._decodeURLFrame(frame);
 
    return undefined;
  }
 
  static _readTimeStamp (timeStampFrame) {
    if (timeStampFrame.data.byteLength === 8) {
      const data = new Uint8Array(timeStampFrame.data);
      // timestamp is 33 bit expressed as a big-endian eight-octet number,
      // with the upper 31 bits set to zero.
      const pts33Bit = data[3] & 0x1;
      let timestamp = (data[4] << 23) +
                      (data[5] << 15) +
                      (data[6] << 7) +
                       data[7];
      timestamp /= 45;
 
      if (pts33Bit)
        timestamp += 47721858.84; // 2^32 / 90
 
      return Math.round(timestamp);
    }
 
    return undefined;
  }
 
  static _decodePrivFrame (frame) {
    /*
    Format: <text string>\0<binary data>
    */
    if (frame.size < 2)
      return undefined;
 
    const owner = ID3._utf8ArrayToStr(frame.data, true);
    const privateData = new Uint8Array(frame.data.subarray(owner.length + 1));
 
    return { key: frame.type, info: owner, data: privateData.buffer };
  }
 
  static _decodeTextFrame (frame) {
    if (frame.size < 2)
      return undefined;
 
    if (frame.type === 'TXXX') {
      /*
      Format:
      [0]   = {Text Encoding}
      [1-?] = {Description}\0{Value}
      */
      let index = 1;
      const description = ID3._utf8ArrayToStr(frame.data.subarray(index));
 
      index += description.length + 1;
      const value = ID3._utf8ArrayToStr(frame.data.subarray(index));
 
      return { key: frame.type, info: description, data: value };
    } else {
      /*
      Format:
      [0]   = {Text Encoding}
      [1-?] = {Value}
      */
      const text = ID3._utf8ArrayToStr(frame.data.subarray(1));
      return { key: frame.type, data: text };
    }
  }
 
  static _decodeURLFrame (frame) {
    if (frame.type === 'WXXX') {
      /*
      Format:
      [0]   = {Text Encoding}
      [1-?] = {Description}\0{URL}
      */
      if (frame.size < 2)
        return undefined;
 
      let index = 1;
      const description = ID3._utf8ArrayToStr(frame.data.subarray(index));
 
      index += description.length + 1;
      const value = ID3._utf8ArrayToStr(frame.data.subarray(index));
 
      return { key: frame.type, info: description, data: value };
    } else {
      /*
      Format:
      [0-?] = {URL}
      */
      const url = ID3._utf8ArrayToStr(frame.data);
      return { key: frame.type, data: url };
    }
  }
 
  // http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197
  // http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt
  /* utf.js - UTF-8 <=> UTF-16 convertion
   *
   * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
   * Version: 1.0
   * LastModified: Dec 25 1999
   * This library is free.  You can redistribute it and/or modify it.
   */
  static _utf8ArrayToStr (array, exitOnNull = false) {
    const len = array.length;
    let c;
    let char2;
    let char3;
    let out = '';
    let i = 0;
    while (i < len) {
      c = array[i++];
      if (c === 0x00 && exitOnNull) {
        return out;
      } else if (c === 0x00 || c === 0x03) {
        // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it
        continue;
      }
      switch (c >> 4) {
      case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
        // 0xxxxxxx
        out += String.fromCharCode(c);
        break;
      case 12: case 13:
        // 110x xxxx   10xx xxxx
        char2 = array[i++];
        out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
        break;
      case 14:
        // 1110 xxxx  10xx xxxx  10xx xxxx
        char2 = array[i++];
        char3 = array[i++];
        out += String.fromCharCode(((c & 0x0F) << 12) |
                    ((char2 & 0x3F) << 6) |
                    ((char3 & 0x3F) << 0));
        break;
      default:
      }
    }
    return out;
  }
}
 
const utf8ArrayToStr = ID3._utf8ArrayToStr;
 
export default ID3;
 
export { utf8ArrayToStr };