async fetchAndAppend_(mediaState, presentationTime, reference, adaptation) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const StreamingEngine = shaka.media.StreamingEngine;
    const logPrefix = StreamingEngine.logPrefix_(mediaState);

    shaka.log.v1(logPrefix,
        'fetchAndAppend_:',
        'presentationTime=' + presentationTime,
        'reference.startTime=' + reference.startTime,
        'reference.endTime=' + reference.endTime);

    // Subtlety: The playhead may move while asynchronous update operations are
    // in progress, so we should avoid calling playhead.getTime() in any
    // callbacks. Furthermore, switch() or seeked() may be called at any time,
    // so we store the old iterator.  This allows the mediaState to change and
    // we'll update the old iterator.
    const stream = mediaState.stream;
    const iter = mediaState.segmentIterator;

    mediaState.performingUpdate = true;

    try {
      if (reference.getStatus() ==
          shaka.media.SegmentReference.Status.MISSING) {
        throw new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.NETWORK,
            shaka.util.Error.Code.SEGMENT_MISSING);
      }
      await this.initSourceBuffer_(mediaState, reference, adaptation);
      this.destroyer_.ensureNotDestroyed();
      if (this.fatalError_) {
        return;
      }

      shaka.log.v2(logPrefix, 'fetching segment');
      const isMP4 = stream.mimeType == 'video/mp4' ||
              stream.mimeType == 'audio/mp4';
      const isReadableStreamSupported = window.ReadableStream;
      const lowLatencyMode = this.config_.lowLatencyMode &&
          this.manifest_.isLowLatency;
      // Enable MP4 low latency streaming with ReadableStream chunked data.
      // And only for DASH and HLS with byterange optimization.
      if (lowLatencyMode && isReadableStreamSupported && isMP4 &&
          (this.manifest_.type != shaka.media.ManifestParser.HLS ||
          reference.hasByterangeOptimization())) {
        let remaining = new Uint8Array(0);
        let processingResult = false;
        let callbackCalled = false;
        let streamDataCallbackError;
        const streamDataCallback = async (data) => {
          if (processingResult) {
            // If the fallback result processing was triggered, don't also
            // append the buffer here.  In theory this should never happen,
            // but it does on some older TVs.
            return;
          }
          callbackCalled = true;
          this.destroyer_.ensureNotDestroyed();
          if (this.fatalError_) {
            return;
          }
          try {
            // Append the data with complete boxes.
            // Every time streamDataCallback gets called, append the new data
            // to the remaining data.
            // Find the last fully completed Mdat box, and slice the data into
            // two parts: the first part with completed Mdat boxes, and the
            // second part with an incomplete box.
            // Append the first part, and save the second part as remaining
            // data, and handle it with the next streamDataCallback call.
            remaining = shaka.util.Uint8ArrayUtils.concat(remaining, data);
            let sawMDAT = false;
            let offset = 0;
            new shaka.util.Mp4Parser()
                .box('mdat', (box) => {
                  offset = box.size + box.start;
                  sawMDAT = true;
                })
                .parse(remaining, /* partialOkay= */ false,
                    /* isChunkedData= */ true);
            if (sawMDAT) {
              const dataToAppend = remaining.subarray(0, offset);
              remaining = remaining.subarray(offset);
              await this.append_(
                  mediaState, presentationTime, stream, reference, dataToAppend,
                  /* isChunkedData= */ true, adaptation);

              if (mediaState.segmentPrefetch && mediaState.segmentIterator) {
                mediaState.segmentPrefetch.prefetchSegmentsByTime(
                    reference.startTime, /* skipFirst= */ true);
              }
            }
          } catch (error) {
            streamDataCallbackError = error;
          }
        };

        const result =
            await this.fetch_(mediaState, reference, streamDataCallback);
        if (streamDataCallbackError) {
          throw streamDataCallbackError;
        }
        if (!callbackCalled) {
          // In some environments, we might be forced to use network plugins
          // that don't support streamDataCallback. In those cases, as a
          // fallback, append the buffer here.
          processingResult = true;
          this.destroyer_.ensureNotDestroyed();
          if (this.fatalError_) {
            return;
          }

          // If the text stream gets switched between fetch_() and append_(),
          // the new text parser is initialized, but the new init segment is
          // not fetched yet.  That would cause an error in
          // TextParser.parseMedia().
          // See http://b/168253400
          if (mediaState.waitingToClearBuffer) {
            shaka.log.info(logPrefix, 'waitingToClearBuffer, skip append');
            mediaState.performingUpdate = false;
            this.scheduleUpdate_(mediaState, 0);
            return;
          }

          await this.append_(mediaState, presentationTime, stream, reference,
              result, /* chunkedData= */ false, adaptation);
        }

        if (mediaState.segmentPrefetch && mediaState.segmentIterator) {
          mediaState.segmentPrefetch.prefetchSegmentsByTime(
              reference.startTime, /* skipFirst= */ true);
        }
      } else {
        if (lowLatencyMode && !isReadableStreamSupported) {
          shaka.log.warning('Low latency streaming mode is enabled, but ' +
            'ReadableStream is not supported by the browser.');
        }
        const fetchSegment = this.fetch_(mediaState, reference);
        const result = await fetchSegment;
        this.destroyer_.ensureNotDestroyed();
        if (this.fatalError_) {
          return;
        }
        this.destroyer_.ensureNotDestroyed();

        // If the text stream gets switched between fetch_() and append_(), the
        // new text parser is initialized, but the new init segment is not
        // fetched yet.  That would cause an error in TextParser.parseMedia().
        // See http://b/168253400
        if (mediaState.waitingToClearBuffer) {
          shaka.log.info(logPrefix, 'waitingToClearBuffer, skip append');
          mediaState.performingUpdate = false;
          this.scheduleUpdate_(mediaState, 0);
          return;
        }

        await this.append_(mediaState, presentationTime, stream, reference,
            result, /* chunkedData= */ false, adaptation);
      }

      this.destroyer_.ensureNotDestroyed();
      if (this.fatalError_) {
        return;
      }
      // move to next segment after appending the current segment.
      mediaState.lastSegmentReference = reference;
      const newRef = iter.next().value;
      shaka.log.v2(logPrefix, 'advancing to next segment', newRef);

      mediaState.performingUpdate = false;
      mediaState.recovering = false;

      const info = this.playerInterface_.mediaSourceEngine.getBufferedInfo();
      const buffered = info[mediaState.type];
      // Convert the buffered object to a string capture its properties on
      // WebOS.
      shaka.log.v1(logPrefix, 'finished fetch and append',
          JSON.stringify(buffered));

      if (!mediaState.waitingToClearBuffer) {
        let otherState = null;
        if (mediaState.type === ContentType.VIDEO) {
          otherState = this.mediaStates_.get(ContentType.AUDIO);
        } else if (mediaState.type === ContentType.AUDIO) {
          otherState = this.mediaStates_.get(ContentType.VIDEO);
        }
        if (otherState && otherState.type == ContentType.AUDIO) {
          this.playerInterface_.onSegmentAppended(reference, mediaState.stream,
              otherState.stream.isAudioMuxedInVideo);
        } else {
          this.playerInterface_.onSegmentAppended(reference, mediaState.stream,
              mediaState.stream.codecs.includes(','));
        }
      }

      // Update right away.
      this.scheduleUpdate_(mediaState, 0);
    } catch (error) {
      this.destroyer_.ensureNotDestroyed(error);
      if (this.fatalError_) {
        return;
      }
      goog.asserts.assert(error instanceof shaka.util.Error,
          'Should only receive a Shaka error');

      mediaState.performingUpdate = false;

      if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
        // If the network slows down, abort the current fetch request and start
        // a new one, and ignore the error message.
        mediaState.performingUpdate = false;
        this.cancelUpdate_(mediaState);
        this.scheduleUpdate_(mediaState, 0);
      } else if (mediaState.type == ContentType.TEXT &&
          this.config_.ignoreTextStreamFailures) {
        if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
          shaka.log.warning(logPrefix,
              'Text stream failed to download. Proceeding without it.');
        } else {
          shaka.log.warning(logPrefix,
              'Text stream failed to parse. Proceeding without it.');
        }
        this.mediaStates_.delete(ContentType.TEXT);
      } else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
        await this.handleQuotaExceeded_(mediaState, error);
      } else {
        shaka.log.error(logPrefix, 'failed fetch and append: code=' +
            error.code);
        mediaState.hasError = true;

        if (error.category == shaka.util.Error.Category.NETWORK &&
            mediaState.segmentPrefetch) {
          mediaState.segmentPrefetch.removeReference(reference);
        }

        error.severity = shaka.util.Error.Severity.CRITICAL;
        await this.handleStreamingError_(mediaState, error);
      }
    }
  }
Fetches and appends the given segment. Sets up the given MediaState's
associated SourceBuffer and evicts segments if either are required
beforehand. Schedules another update after completing successfully.

@param {!shaka.media.StreamingEngine.MediaState_} mediaState
@param {number} presentationTime
@param {!shaka.media.SegmentReference} reference
@param {boolean} adaptation
@private