static fromUTF8(data) {
    if (!data) {
      return '';
    }

    let uint8 = shaka.util.BufferUtils.toUint8(data);
    // If present, strip off the UTF-8 BOM.
    if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) {
      uint8 = uint8.subarray(3);
    }

    if (window.TextDecoder && !shaka.device.DeviceFactory.getDevice()
        .shouldAvoidUseTextDecoderEncoder()) {
      // Use the TextDecoder interface to decode the text.  This has the
      // advantage compared to the previously-standard decodeUriComponent that
      // it will continue parsing even if it finds an invalid UTF8 character,
      // rather than stop and throw an error.
      const utf8decoder = new TextDecoder();
      const decoded = utf8decoder.decode(uint8);
      if (decoded.includes('\uFFFD')) {
        shaka.log.alwaysError('Decoded string contains an "unknown character' +
                              '" codepoint.  That probably means the UTF8 ' +
                              'encoding was incorrect!');
      }
      return decoded;
    } else {
      // Homebrewed UTF-8 decoder based on
      // https://en.wikipedia.org/wiki/UTF-8#Encoding
      // Unlike decodeURIComponent, won't throw on bad encoding.
      // In this way, it is similar to TextDecoder.

      let decoded = '';
      for (let i = 0; i < uint8.length; ++i) {
        // By default, the "replacement character" codepoint.
        let codePoint = 0xFFFD;

        // Top bit is 0, 1-byte encoding.
        if ((uint8[i] & 0x80) == 0) {
          codePoint = uint8[i];

        // Top 3 bits of byte 0 are 110, top 2 bits of byte 1 are 10,
        // 2-byte encoding.
        } else if (uint8.length >= i + 2 &&
            (uint8[i] & 0xe0) == 0xc0 &&
            (uint8[i + 1] & 0xc0) == 0x80) {
          codePoint = ((uint8[i] & 0x1f) << 6) |
                      ((uint8[i + 1] & 0x3f));
          i += 1;  // Consume one extra byte.

        // Top 4 bits of byte 0 are 1110, top 2 bits of byte 1 and 2 are 10,
        // 3-byte encoding.
        } else if (uint8.length >= i + 3 &&
            (uint8[i] & 0xf0) == 0xe0 &&
            (uint8[i + 1] & 0xc0) == 0x80 &&
            (uint8[i + 2] & 0xc0) == 0x80) {
          codePoint = ((uint8[i] & 0x0f) << 12) |
                      ((uint8[i + 1] & 0x3f) << 6) |
                      ((uint8[i + 2] & 0x3f));
          i += 2;  // Consume two extra bytes.

        // Top 5 bits of byte 0 are 11110, top 2 bits of byte 1, 2 and 3 are 10,
        // 4-byte encoding.
        } else if (uint8.length >= i + 4 &&
            (uint8[i] & 0xf1) == 0xf0 &&
            (uint8[i + 1] & 0xc0) == 0x80 &&
            (uint8[i + 2] & 0xc0) == 0x80 &&
            (uint8[i + 3] & 0xc0) == 0x80) {
          codePoint = ((uint8[i] & 0x07) << 18) |
                      ((uint8[i + 1] & 0x3f) << 12) |
                      ((uint8[i + 2] & 0x3f) << 6) |
                      ((uint8[i + 3] & 0x3f));
          i += 3;  // Consume three extra bytes.
        }

        // JavaScript strings are a series of UTF-16 characters.
        if (codePoint <= 0xffff) {
          decoded += String.fromCharCode(codePoint);
        } else {
          // UTF-16 surrogate-pair encoding, based on
          // https://en.wikipedia.org/wiki/UTF-16#Description
          const baseCodePoint = codePoint - 0x10000;
          const highPart = baseCodePoint >> 10;
          const lowPart = baseCodePoint & 0x3ff;
          decoded += String.fromCharCode(0xd800 + highPart);
          decoded += String.fromCharCode(0xdc00 + lowPart);
        }
      }

      return decoded;
    }
  }
Creates a string from the given buffer as UTF-8 encoding.

@param {?BufferSource} data
@return {string}
@export