text: /**
   * 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);
      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) {
  
      const len = array.length;
      let c;
      let char2;
      let char3;
      let out = '';
      let i = 0;
      while (i < len) {
          c = array[i++];
          // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it
          if (c === 0x00 || c === 0x03) {
              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 };
  