async appendBuffer(
      contentType, data, reference, stream, hasClosedCaptions, seeked = false,
      adaptation = false, isChunkedData = false, fromSplit = false,
      continuityTimeline) {
    const ContentType = shaka.util.ManifestParserUtils.ContentType;

    if (contentType == ContentType.TEXT) {
      if (this.manifestType_ == shaka.media.ManifestParser.HLS) {
        // This won't be known until the first video segment is appended.
        const offset = await this.textSequenceModeOffset_;
        this.textEngine_.setTimestampOffset(offset);
      }
      await this.textEngine_.appendBuffer(
          data,
          reference ? reference.startTime : null,
          reference ? reference.endTime : null,
          reference ? reference.getUris()[0] : null);
      return;
    }

    if (!fromSplit && this.needSplitMuxedContent_) {
      await this.appendBuffer(ContentType.AUDIO, data, reference, stream,
          hasClosedCaptions, seeked, adaptation, isChunkedData,
          /* fromSplit= */ true);
      await this.appendBuffer(ContentType.VIDEO, data, reference, stream,
          hasClosedCaptions, seeked, adaptation, isChunkedData,
          /* fromSplit= */ true);
      return;
    }

    if (!this.sourceBuffers_.has(contentType)) {
      shaka.log.warning('Attempted to restore a non-existent source buffer');
      return;
    }

    let timestampOffset = this.sourceBuffers_.get(contentType).timestampOffset;

    let mimeType = this.sourceBufferTypes_.get(contentType);
    if (this.transmuxers_.has(contentType)) {
      mimeType = this.transmuxers_.get(contentType).getOriginalMimeType();
    }
    if (reference) {
      const {timestamp, metadata} = this.getTimestampAndDispatchMetadata(
          contentType, data, reference, stream, mimeType);
      if (timestamp != null) {
        if (this.firstVideoTimestamp_ == null &&
            contentType == ContentType.VIDEO) {
          this.firstVideoTimestamp_ = timestamp;
          this.firstVideoReferenceStartTime_ = reference.startTime;
          if (this.firstAudioTimestamp_ != null) {
            let compensation = 0;
            // Only apply compensation if video and audio segment startTime
            // match, to avoid introducing sync issues.
            if (this.firstVideoReferenceStartTime_ ==
                this.firstAudioReferenceStartTime_) {
              compensation =
                  this.firstVideoTimestamp_ - this.firstAudioTimestamp_;
            }
            this.audioCompensation_.resolve(compensation);
          }
        }
        if (this.firstAudioTimestamp_ == null &&
            contentType == ContentType.AUDIO) {
          this.firstAudioTimestamp_ = timestamp;
          this.firstAudioReferenceStartTime_ = reference.startTime;
          if (this.firstVideoTimestamp_ != null) {
            let compensation = 0;
            // Only apply compensation if video and audio segment startTime
            // match, to avoid introducing sync issues.
            if (this.firstVideoReferenceStartTime_ ==
                this.firstAudioReferenceStartTime_) {
              compensation =
                  this.firstVideoTimestamp_ - this.firstAudioTimestamp_;
            }
            this.audioCompensation_.resolve(compensation);
          }
        }
        let realTimestamp = timestamp;
        const RAW_FORMATS = shaka.util.MimeUtils.RAW_FORMATS;
        // For formats without containers and using segments mode, we need to
        // adjust TimestampOffset relative to 0 because segments do not have
        // any timestamp information.
        if (!this.sequenceMode_ &&
            RAW_FORMATS.includes(this.sourceBufferTypes_.get(contentType))) {
          realTimestamp = 0;
        }
        const calculatedTimestampOffset = reference.startTime - realTimestamp;
        const timestampOffsetDifference =
            Math.abs(timestampOffset - calculatedTimestampOffset);
        if ((timestampOffsetDifference >= 0.001 || seeked || adaptation) &&
            (!isChunkedData || calculatedTimestampOffset > 0 ||
            !timestampOffset)) {
          timestampOffset = calculatedTimestampOffset;
          if (this.attemptTimestampOffsetCalculation_) {
            this.enqueueOperation_(
                contentType,
                () => this.abort_(contentType),
                null);
            this.enqueueOperation_(
                contentType,
                () => this.setTimestampOffset_(contentType, timestampOffset),
                null);
          }
        }
        // Timestamps can only be reliably extracted from video, not audio.
        // Packed audio formats do not have internal timestamps at all.
        // Prefer video for this when available.
        const isBestSourceBufferForTimestamps =
            contentType == ContentType.VIDEO ||
            !(this.sourceBuffers_.has(ContentType.VIDEO));
        if (isBestSourceBufferForTimestamps) {
          this.textSequenceModeOffset_.resolve(timestampOffset);
        }
      }
      if (metadata.length) {
        this.playerInterface_.onMetadata(metadata, timestampOffset,
            reference ? reference.endTime : null);
      }
    }
    if (hasClosedCaptions && contentType == ContentType.VIDEO) {
      if (!this.textEngine_) {
        this.reinitText(shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE,
            this.sequenceMode_, /* external= */ false);
      }
      if (!this.captionParser_) {
        const basicType = mimeType.split(';', 1)[0];
        this.captionParser_ = this.getCaptionParser(basicType);
      }
      // If it is the init segment for closed captions, initialize the closed
      // caption parser.
      if (!reference) {
        this.captionParser_.init(data, adaptation, continuityTimeline);
      } else {
        const closedCaptions = this.captionParser_.parseFrom(data);
        if (closedCaptions.length) {
          this.textEngine_.storeAndAppendClosedCaptions(
              closedCaptions,
              reference.startTime,
              reference.endTime,
              timestampOffset);
        }
      }
    }

    if (this.transmuxers_.has(contentType)) {
      data = await this.transmuxers_.get(contentType).transmux(
          data, stream, reference, this.mediaSource_.duration, contentType);
    }

    data = this.workAroundBrokenPlatforms_(
        stream, data, reference, contentType);

    if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
      // In sequence mode, for non-text streams, if we just cleared the buffer
      // and are either performing an unbuffered seek or handling an automatic
      // adaptation, we need to set a new timestampOffset on the sourceBuffer.
      if (seeked || adaptation) {
        let timestampOffset = reference.startTime;
        // Audio and video may not be aligned, so we will compensate for audio
        // if necessary.
        if (this.manifestType_ == shaka.media.ManifestParser.HLS &&
            !this.needSplitMuxedContent_ &&
            contentType == ContentType.AUDIO &&
            this.sourceBuffers_.has(ContentType.VIDEO)) {
          const compensation = await this.audioCompensation_;
          // Only apply compensation if the difference is greater than 150ms
          if (Math.abs(compensation) > 0.15) {
            timestampOffset -= compensation;
          }
        }
        // The logic to call abort() before setting the timestampOffset is
        // extended during unbuffered seeks or automatic adaptations; it is
        // possible for the append state to be PARSING_MEDIA_SEGMENT from the
        // previous SourceBuffer#appendBuffer() call.
        this.enqueueOperation_(
            contentType,
            () => this.abort_(contentType),
            null);
        this.enqueueOperation_(
            contentType,
            () => this.setTimestampOffset_(contentType, timestampOffset),
            null);
      }
    }

    let bufferedBefore = null;

    await this.enqueueOperation_(contentType, () => {
      if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
        bufferedBefore = this.getBuffered_(contentType);
      }
      this.append_(contentType, data, timestampOffset, stream);
    }, reference ? reference.getUris()[0] : null);

    if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
      const bufferedAfter = this.getBuffered_(contentType);
      const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange(
          bufferedBefore, bufferedAfter);
      if (newBuffered) {
        const segmentDuration = reference.endTime - reference.startTime;
        const timeAdded = newBuffered.end - newBuffered.start;
        // Check end times instead of start times.  We may be overwriting a
        // buffer and only the end changes, and that would be fine.
        // Also, exclude tiny segments.  Sometimes alignment segments as small
        // as 33ms are seen in Google DAI content.  For such tiny segments,
        // half a segment duration would be no issue.
        const offset = Math.abs(newBuffered.end - reference.endTime);
        if (segmentDuration > 0.100 && (offset > segmentDuration / 2 ||
            Math.abs(segmentDuration - timeAdded) > 0.030)) {
          shaka.log.error('Possible encoding problem detected!',
              'Unexpected buffered range for reference', reference,
              'from URIs', reference.getUris(),
              'should be', {start: reference.startTime, end: reference.endTime},
              'but got', newBuffered);
        }
      }
    }
  }
Enqueue an operation to append data to the SourceBuffer.
Start and end times are needed for TextEngine, but not for MediaSource.
Start and end times may be null for initialization segments; if present
they are relative to the presentation timeline.

@param {shaka.util.ManifestParserUtils.ContentType} contentType
@param {!BufferSource} data
@param {?shaka.media.SegmentReference} reference The segment reference
  we are appending, or null for init segments
@param {shaka.extern.Stream} stream
@param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
  captions
@param {boolean=} seeked True if we just seeked
@param {boolean=} adaptation True if we just automatically switched active
  variant(s).
@param {boolean=} isChunkedData True if we add to the buffer from the
@param {boolean=} fromSplit
@param {number=} continuityTimeline an optional continuity timeline
@return {!Promise}