public class

SampleQueue

extends java.lang.Object

implements TrackOutput

 java.lang.Object

↳androidx.media3.exoplayer.source.SampleQueue

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

A queue of media samples.

Summary

Constructors
protectedSampleQueue(Allocator allocator, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

Methods
public static SampleQueuecreateWithDrm(Allocator allocator, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

Creates a sample queue with DRM resource management.

public static SampleQueuecreateWithDrm(Allocator allocator, Looper playbackLooper, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

public static SampleQueuecreateWithoutDrm(Allocator allocator)

Creates a sample queue without DRM resource management.

public synchronized longdiscardSampleMetadataToRead()

public final voiddiscardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition)

Discards up to but not including the sample immediately before or at the specified time.

public final voiddiscardToEnd()

Discards all samples in the queue and advances the read position.

public final voiddiscardToRead()

Discards up to but not including the read position.

public final voiddiscardUpstreamFrom(long timeUs)

Discards samples from the write side of the queue.

public final voiddiscardUpstreamSamples(int discardFromIndex)

Discards samples from the write side of the queue.

public final voidformat(Format unadjustedUpstreamFormat)

protected FormatgetAdjustedUpstreamFormat(Format format)

Adjusts the upstream Format (i.e., the Format that was most recently passed to SampleQueue.format(Format)).

public final intgetFirstIndex()

Returns the current absolute start index.

public final synchronized longgetFirstTimestampUs()

Returns the timestamp of the first sample, or MIN_VALUE if the queue is empty.

public final synchronized longgetLargestQueuedTimestampUs()

Returns the largest sample timestamp that has been queued since the last SampleQueue.reset().

public final synchronized longgetLargestReadTimestampUs()

Returns the largest sample timestamp that has been read since the last SampleQueue.reset().

public final intgetReadIndex()

Returns the current absolute read index.

public final synchronized intgetSkipCount(long timeUs, boolean allowEndOfQueue)

Returns the number of samples that need to be skipped to advance the read position to the keyframe before or at the specified time.

public final synchronized FormatgetUpstreamFormat()

Returns the upstream Format in which samples are being queued.

public final intgetWriteIndex()

Returns the current absolute write index.

protected final voidinvalidateUpstreamFormatAdjustment()

Invalidates the last upstream format adjustment.

public final synchronized booleanisLastSampleQueued()

Returns whether the last sample of the stream has knowingly been queued.

public synchronized booleanisReady(boolean loadingFinished)

Returns whether there is data available for reading.

public voidmaybeThrowError()

Throws an error that's preventing data from being read.

public final synchronized intpeekSourceId()

Peeks the source id of the next sample to be read, or the current upstream source id if the queue is empty or if the read position is at the end of the queue.

public voidpreRelease()

Calls SampleQueue.discardToEnd() and releases any resources owned by the queue.

public intread(FormatHolder formatHolder, DecoderInputBuffer buffer, int readFlags, boolean loadingFinished)

Attempts to read from the queue.

public voidrelease()

Calls reset(true) and releases any resources owned by the queue.

public final voidreset()

Convenience method for reset(false).

public voidreset(boolean resetUpstreamFormat)

Clears all samples from the queue.

public final intsampleData(DataReader input, int length, boolean allowEndOfInput, int sampleDataPart)

public final voidsampleData(ParsableByteArray data, int length, int sampleDataPart)

public voidsampleMetadata(long timeUs, int flags, int size, int offset, TrackOutput.CryptoData cryptoData)

public final synchronized booleanseekTo(int sampleIndex)

Attempts to seek the read position to the specified sample index.

public final synchronized booleanseekTo(long timeUs, boolean allowTimeBeyondBuffer)

Attempts to seek the read position to the keyframe before or at the specified time.

public final voidsetSampleOffsetUs(long sampleOffsetUs)

Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that are subsequently queued.

public final voidsetStartTimeUs(long startTimeUs)

Sets the start time for the queue.

public final voidsetUpstreamFormatChangeListener(SampleQueue.UpstreamFormatChangedListener listener)

Sets a listener to be notified of changes to the upstream format.

public final synchronized voidskip(int count)

Advances the read position by the specified number of samples.

public final voidsourceId(int sourceId)

Sets a source identifier for subsequent samples.

public final voidsplice()

Indicates samples that are subsequently queued should be spliced into those already queued.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

protected SampleQueue(Allocator allocator, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

Methods

public static SampleQueue createWithoutDrm(Allocator allocator)

Creates a sample queue without DRM resource management.

Parameters:

allocator: An Allocator from which allocations for sample data can be obtained.

public static SampleQueue createWithDrm(Allocator allocator, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

Creates a sample queue with DRM resource management.

For each sample added to the queue, a DrmSession will be attached containing the keys needed to decrypt it.

Parameters:

allocator: An Allocator from which allocations for sample data can be obtained.
drmSessionManager: The DrmSessionManager to obtain DrmSessions from. The created instance does not take ownership of this DrmSessionManager.
drmEventDispatcher: A to notify of events related to this SampleQueue.

public static SampleQueue createWithDrm(Allocator allocator, Looper playbackLooper, DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher)

Deprecated: Use SampleQueue instead. The playbackLooper should be configured on the DrmSessionManager with DrmSessionManager.setPlayer(Looper, PlayerId).

public void release()

Calls reset(true) and releases any resources owned by the queue.

public final void reset()

Convenience method for reset(false).

public void reset(boolean resetUpstreamFormat)

Clears all samples from the queue.

Parameters:

resetUpstreamFormat: Whether the upstream format should be cleared. If set to false, samples queued after the reset (and before a subsequent call to SampleQueue.format(Format)) are assumed to have the current upstream format. If set to true, SampleQueue.format(Format) must be called after the reset before any more samples can be queued.

public final void setStartTimeUs(long startTimeUs)

Sets the start time for the queue. Samples with earlier timestamps will be discarded or have the C.BUFFER_FLAG_DECODE_ONLY flag set when read.

Parameters:

startTimeUs: The start time, in microseconds.

public final void sourceId(int sourceId)

Sets a source identifier for subsequent samples.

Parameters:

sourceId: The source identifier.

public final void splice()

Indicates samples that are subsequently queued should be spliced into those already queued.

public final int getWriteIndex()

Returns the current absolute write index.

public final void discardUpstreamSamples(int discardFromIndex)

Discards samples from the write side of the queue.

Parameters:

discardFromIndex: The absolute index of the first sample to be discarded. Must be in the range [SampleQueue.getReadIndex(), SampleQueue.getWriteIndex()].

public final void discardUpstreamFrom(long timeUs)

Discards samples from the write side of the queue.

Parameters:

timeUs: Samples will be discarded from the write end of the queue until a sample with a timestamp smaller than timeUs is encountered (this sample is not discarded). Must be larger than SampleQueue.getLargestReadTimestampUs().

public void preRelease()

Calls SampleQueue.discardToEnd() and releases any resources owned by the queue.

public void maybeThrowError()

Throws an error that's preventing data from being read. Does nothing if no such error exists.

public final int getFirstIndex()

Returns the current absolute start index.

public final int getReadIndex()

Returns the current absolute read index.

public final synchronized int peekSourceId()

Peeks the source id of the next sample to be read, or the current upstream source id if the queue is empty or if the read position is at the end of the queue.

Returns:

The source id.

public final synchronized Format getUpstreamFormat()

Returns the upstream Format in which samples are being queued.

public final synchronized long getLargestQueuedTimestampUs()

Returns the largest sample timestamp that has been queued since the last SampleQueue.reset().

Samples that were discarded by calling SampleQueue.discardUpstreamSamples(int) are not considered as having been queued. Samples that were dequeued from the front of the queue are considered as having been queued.

Returns:

The largest sample timestamp that has been queued, or MIN_VALUE if no samples have been queued.

public final synchronized long getLargestReadTimestampUs()

Returns the largest sample timestamp that has been read since the last SampleQueue.reset().

Returns:

The largest sample timestamp that has been read, or MIN_VALUE if no samples have been read.

public final synchronized boolean isLastSampleQueued()

Returns whether the last sample of the stream has knowingly been queued. A return value of false means that the last sample had not been queued or that it's unknown whether the last sample has been queued.

Samples that were discarded by calling SampleQueue.discardUpstreamSamples(int) are not considered as having been queued. Samples that were dequeued from the front of the queue are considered as having been queued.

public final synchronized long getFirstTimestampUs()

Returns the timestamp of the first sample, or MIN_VALUE if the queue is empty.

public synchronized boolean isReady(boolean loadingFinished)

Returns whether there is data available for reading.

Note: If the stream has ended then a buffer with the end of stream flag can always be read from SampleQueue.read(FormatHolder, DecoderInputBuffer, int, boolean). Hence an ended stream is always ready.

Parameters:

loadingFinished: Whether no more samples will be written to the sample queue. When true, this method returns true if the sample queue is empty, because an empty sample queue means the end of stream has been reached. When false, this method returns false if the sample queue is empty.

public int read(FormatHolder formatHolder, DecoderInputBuffer buffer, int readFlags, boolean loadingFinished)

Attempts to read from the queue.

Formats read from this method may be associated to a DrmSession through FormatHolder.drmSession.

Parameters:

formatHolder: A FormatHolder to populate in the case of reading a format.
buffer: A DecoderInputBuffer to populate in the case of reading a sample or the end of the stream. If the end of the stream has been reached, the C.BUFFER_FLAG_END_OF_STREAM flag will be set on the buffer.
readFlags: Flags controlling the behavior of this read operation.
loadingFinished: True if an empty queue should be considered the end of the stream.

Returns:

The result, which can be C.RESULT_NOTHING_READ, C.RESULT_FORMAT_READ or C.RESULT_BUFFER_READ.

public final synchronized boolean seekTo(int sampleIndex)

Attempts to seek the read position to the specified sample index.

Parameters:

sampleIndex: The sample index.

Returns:

Whether the seek was successful.

public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer)

Attempts to seek the read position to the keyframe before or at the specified time.

Parameters:

timeUs: The time to seek to.
allowTimeBeyondBuffer: Whether the operation can succeed if timeUs is beyond the end of the queue, by seeking to the last sample (or keyframe).

Returns:

Whether the seek was successful.

public final synchronized int getSkipCount(long timeUs, boolean allowEndOfQueue)

Returns the number of samples that need to be skipped to advance the read position to the keyframe before or at the specified time.

Parameters:

timeUs: The time to advance to.
allowEndOfQueue: Whether the end of the queue is considered a keyframe when timeUs is larger than the largest queued timestamp.

Returns:

The number of samples that need to be skipped, which may be equal to 0.

public final synchronized void skip(int count)

Advances the read position by the specified number of samples.

Parameters:

count: The number of samples to advance the read position by. Must be at least 0 and at most SampleQueue.getWriteIndex() - SampleQueue.getReadIndex().

public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition)

Discards up to but not including the sample immediately before or at the specified time.

Parameters:

timeUs: The time to discard up to.
toKeyframe: If true then discards samples up to the keyframe before or at the specified time, rather than any sample before or at that time.
stopAtReadPosition: If true then samples are only discarded if they're before the read position. If false then samples at and beyond the read position may be discarded, in which case the read position is advanced to the first remaining sample.

public final void discardToRead()

Discards up to but not including the read position.

public final void discardToEnd()

Discards all samples in the queue and advances the read position.

public final void setSampleOffsetUs(long sampleOffsetUs)

Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that are subsequently queued.

Parameters:

sampleOffsetUs: The timestamp offset in microseconds.

public final void setUpstreamFormatChangeListener(SampleQueue.UpstreamFormatChangedListener listener)

Sets a listener to be notified of changes to the upstream format.

Parameters:

listener: The listener.

public final void format(Format unadjustedUpstreamFormat)

public final int sampleData(DataReader input, int length, boolean allowEndOfInput, int sampleDataPart)

public final void sampleData(ParsableByteArray data, int length, int sampleDataPart)

public void sampleMetadata(long timeUs, int flags, int size, int offset, TrackOutput.CryptoData cryptoData)

protected final void invalidateUpstreamFormatAdjustment()

Invalidates the last upstream format adjustment. SampleQueue.getAdjustedUpstreamFormat(Format) will be called to adjust the upstream Format again before the next sample is queued.

protected Format getAdjustedUpstreamFormat(Format format)

Adjusts the upstream Format (i.e., the Format that was most recently passed to SampleQueue.format(Format)).

The default implementation incorporates the sample offset passed to SampleQueue.setSampleOffsetUs(long) into Format.subsampleOffsetUs.

Parameters:

format: The Format to adjust.

Returns:

The adjusted Format.

public synchronized long discardSampleMetadataToRead()

Source

/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.exoplayer.source;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_OMIT_SAMPLE_DATA;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_PEEK;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import static java.lang.Math.max;

import android.os.Looper;
import androidx.annotation.CallSuper;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.DataReader;
import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.ParsableByteArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.decoder.DecoderInputBuffer.InsufficientCapacityException;
import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener.EventDispatcher;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.drm.DrmSessionManager.DrmSessionReference;
import androidx.media3.exoplayer.source.SampleStream.ReadFlags;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.extractor.TrackOutput;
import java.io.IOException;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/** A queue of media samples. */
@UnstableApi
public class SampleQueue implements TrackOutput {

  /** A listener for changes to the upstream format. */
  public interface UpstreamFormatChangedListener {

    /**
     * Called on the loading thread when an upstream format change occurs.
     *
     * @param format The new upstream format.
     */
    void onUpstreamFormatChanged(Format format);
  }

  @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000;
  private static final String TAG = "SampleQueue";

  private final SampleDataQueue sampleDataQueue;
  private final SampleExtrasHolder extrasHolder;
  private final SpannedData<SharedSampleMetadata> sharedSampleMetadata;
  @Nullable private final DrmSessionManager drmSessionManager;
  @Nullable private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
  @Nullable private UpstreamFormatChangedListener upstreamFormatChangeListener;

  @Nullable private Format downstreamFormat;
  @Nullable private DrmSession currentDrmSession;

  private int capacity;
  private int[] sourceIds;
  private long[] offsets;
  private int[] sizes;
  private int[] flags;
  private long[] timesUs;
  private @NullableType CryptoData[] cryptoDatas;

  private int length;
  private int absoluteFirstIndex;
  private int relativeFirstIndex;
  private int readPosition;

  private long startTimeUs;
  private long largestDiscardedTimestampUs;
  private long largestQueuedTimestampUs;
  private boolean isLastSampleQueued;
  private boolean upstreamKeyframeRequired;
  private boolean upstreamFormatRequired;
  private boolean upstreamFormatAdjustmentRequired;
  @Nullable private Format unadjustedUpstreamFormat;
  @Nullable private Format upstreamFormat;
  private int upstreamSourceId;
  private boolean upstreamAllSamplesAreSyncSamples;
  private boolean loggedUnexpectedNonSyncSample;

  private long sampleOffsetUs;
  private boolean pendingSplice;

  /**
   * Creates a sample queue without DRM resource management.
   *
   * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
   */
  public static SampleQueue createWithoutDrm(Allocator allocator) {
    return new SampleQueue(
        allocator, /* drmSessionManager= */ null, /* drmEventDispatcher= */ null);
  }

  /**
   * Creates a sample queue with DRM resource management.
   *
   * <p>For each sample added to the queue, a {@link DrmSession} will be attached containing the
   * keys needed to decrypt it.
   *
   * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
   * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions}
   *     from. The created instance does not take ownership of this {@link DrmSessionManager}.
   * @param drmEventDispatcher A {@link DrmSessionEventListener.EventDispatcher} to notify of events
   *     related to this SampleQueue.
   */
  public static SampleQueue createWithDrm(
      Allocator allocator,
      DrmSessionManager drmSessionManager,
      DrmSessionEventListener.EventDispatcher drmEventDispatcher) {
    return new SampleQueue(
        allocator,
        Assertions.checkNotNull(drmSessionManager),
        Assertions.checkNotNull(drmEventDispatcher));
  }

  /**
   * @deprecated Use {@link #createWithDrm(Allocator, DrmSessionManager, EventDispatcher)} instead.
   *     The {@code playbackLooper} should be configured on the {@link DrmSessionManager} with
   *     {@link DrmSessionManager#setPlayer(Looper, PlayerId)}.
   */
  @Deprecated
  public static SampleQueue createWithDrm(
      Allocator allocator,
      Looper playbackLooper,
      DrmSessionManager drmSessionManager,
      DrmSessionEventListener.EventDispatcher drmEventDispatcher) {
    drmSessionManager.setPlayer(playbackLooper, PlayerId.UNSET);
    return new SampleQueue(
        allocator,
        Assertions.checkNotNull(drmSessionManager),
        Assertions.checkNotNull(drmEventDispatcher));
  }

  protected SampleQueue(
      Allocator allocator,
      @Nullable DrmSessionManager drmSessionManager,
      @Nullable DrmSessionEventListener.EventDispatcher drmEventDispatcher) {
    this.drmSessionManager = drmSessionManager;
    this.drmEventDispatcher = drmEventDispatcher;
    sampleDataQueue = new SampleDataQueue(allocator);
    extrasHolder = new SampleExtrasHolder();
    capacity = SAMPLE_CAPACITY_INCREMENT;
    sourceIds = new int[capacity];
    offsets = new long[capacity];
    timesUs = new long[capacity];
    flags = new int[capacity];
    sizes = new int[capacity];
    cryptoDatas = new CryptoData[capacity];
    sharedSampleMetadata =
        new SpannedData<>(/* removeCallback= */ metadata -> metadata.drmSessionReference.release());
    startTimeUs = Long.MIN_VALUE;
    largestDiscardedTimestampUs = Long.MIN_VALUE;
    largestQueuedTimestampUs = Long.MIN_VALUE;
    upstreamFormatRequired = true;
    upstreamKeyframeRequired = true;
  }

  // Called by the consuming thread when there is no loading thread.

  /** Calls {@link #reset(boolean) reset(true)} and releases any resources owned by the queue. */
  @CallSuper
  public void release() {
    reset(/* resetUpstreamFormat= */ true);
    releaseDrmSessionReferences();
  }

  /** Convenience method for {@code reset(false)}. */
  public final void reset() {
    reset(/* resetUpstreamFormat= */ false);
  }

  /**
   * Clears all samples from the queue.
   *
   * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false,
   *     samples queued after the reset (and before a subsequent call to {@link #format(Format)})
   *     are assumed to have the current upstream format. If set to true, {@link #format(Format)}
   *     must be called after the reset before any more samples can be queued.
   */
  @CallSuper
  public void reset(boolean resetUpstreamFormat) {
    sampleDataQueue.reset();
    length = 0;
    absoluteFirstIndex = 0;
    relativeFirstIndex = 0;
    readPosition = 0;
    upstreamKeyframeRequired = true;
    startTimeUs = Long.MIN_VALUE;
    largestDiscardedTimestampUs = Long.MIN_VALUE;
    largestQueuedTimestampUs = Long.MIN_VALUE;
    isLastSampleQueued = false;
    sharedSampleMetadata.clear();
    if (resetUpstreamFormat) {
      unadjustedUpstreamFormat = null;
      upstreamFormat = null;
      upstreamFormatRequired = true;
    }
  }

  /**
   * Sets the start time for the queue. Samples with earlier timestamps will be discarded or have
   * the {@link C#BUFFER_FLAG_DECODE_ONLY} flag set when read.
   *
   * @param startTimeUs The start time, in microseconds.
   */
  public final void setStartTimeUs(long startTimeUs) {
    this.startTimeUs = startTimeUs;
  }

  /**
   * Sets a source identifier for subsequent samples.
   *
   * @param sourceId The source identifier.
   */
  public final void sourceId(int sourceId) {
    upstreamSourceId = sourceId;
  }

  /** Indicates samples that are subsequently queued should be spliced into those already queued. */
  public final void splice() {
    pendingSplice = true;
  }

  /** Returns the current absolute write index. */
  public final int getWriteIndex() {
    return absoluteFirstIndex + length;
  }

  /**
   * Discards samples from the write side of the queue.
   *
   * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the
   *     range [{@link #getReadIndex()}, {@link #getWriteIndex()}].
   */
  public final void discardUpstreamSamples(int discardFromIndex) {
    sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex));
  }

  /**
   * Discards samples from the write side of the queue.
   *
   * @param timeUs Samples will be discarded from the write end of the queue until a sample with a
   *     timestamp smaller than timeUs is encountered (this sample is not discarded). Must be larger
   *     than {@link #getLargestReadTimestampUs()}.
   */
  public final void discardUpstreamFrom(long timeUs) {
    if (length == 0) {
      return;
    }
    checkArgument(timeUs > getLargestReadTimestampUs());
    int retainCount = countUnreadSamplesBefore(timeUs);
    discardUpstreamSamples(absoluteFirstIndex + retainCount);
  }

  // Called by the consuming thread.

  /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */
  @CallSuper
  public void preRelease() {
    discardToEnd();
    releaseDrmSessionReferences();
  }

  /**
   * Throws an error that's preventing data from being read. Does nothing if no such error exists.
   *
   * @throws IOException The underlying error.
   */
  @CallSuper
  public void maybeThrowError() throws IOException {
    // TODO: Avoid throwing if the DRM error is not preventing a read operation.
    if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) {
      throw Assertions.checkNotNull(currentDrmSession.getError());
    }
  }

  /** Returns the current absolute start index. */
  public final int getFirstIndex() {
    return absoluteFirstIndex;
  }

  /** Returns the current absolute read index. */
  public final int getReadIndex() {
    return absoluteFirstIndex + readPosition;
  }

  /**
   * Peeks the source id of the next sample to be read, or the current upstream source id if the
   * queue is empty or if the read position is at the end of the queue.
   *
   * @return The source id.
   */
  public final synchronized int peekSourceId() {
    int relativeReadIndex = getRelativeIndex(readPosition);
    return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId;
  }

  /** Returns the upstream {@link Format} in which samples are being queued. */
  @Nullable
  public final synchronized Format getUpstreamFormat() {
    return upstreamFormatRequired ? null : upstreamFormat;
  }

  /**
   * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
   *
   * <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
   * considered as having been queued. Samples that were dequeued from the front of the queue are
   * considered as having been queued.
   *
   * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no
   *     samples have been queued.
   */
  public final synchronized long getLargestQueuedTimestampUs() {
    return largestQueuedTimestampUs;
  }

  /**
   * Returns the largest sample timestamp that has been read since the last {@link #reset}.
   *
   * @return The largest sample timestamp that has been read, or {@link Long#MIN_VALUE} if no
   *     samples have been read.
   */
  public final synchronized long getLargestReadTimestampUs() {
    return max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition));
  }

  /**
   * Returns whether the last sample of the stream has knowingly been queued. A return value of
   * {@code false} means that the last sample had not been queued or that it's unknown whether the
   * last sample has been queued.
   *
   * <p>Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not
   * considered as having been queued. Samples that were dequeued from the front of the queue are
   * considered as having been queued.
   */
  public final synchronized boolean isLastSampleQueued() {
    return isLastSampleQueued;
  }

  /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
  public final synchronized long getFirstTimestampUs() {
    return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
  }

  /**
   * Returns whether there is data available for reading.
   *
   * <p>Note: If the stream has ended then a buffer with the end of stream flag can always be read
   * from {@link #read}. Hence an ended stream is always ready.
   *
   * @param loadingFinished Whether no more samples will be written to the sample queue. When true,
   *     this method returns true if the sample queue is empty, because an empty sample queue means
   *     the end of stream has been reached. When false, this method returns false if the sample
   *     queue is empty.
   */
  @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat
  @CallSuper
  public synchronized boolean isReady(boolean loadingFinished) {
    if (!hasNextSample()) {
      return loadingFinished
          || isLastSampleQueued
          || (upstreamFormat != null && upstreamFormat != downstreamFormat);
    }
    if (sharedSampleMetadata.get(getReadIndex()).format != downstreamFormat) {
      // A format can be read.
      return true;
    }
    return mayReadSample(getRelativeIndex(readPosition));
  }

  /**
   * Attempts to read from the queue.
   *
   * <p>{@link Format Formats} read from this method may be associated to a {@link DrmSession}
   * through {@link FormatHolder#drmSession}.
   *
   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
   *     end of the stream. If the end of the stream has been reached, the {@link
   *     C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
   * @param readFlags Flags controlling the behavior of this read operation.
   * @param loadingFinished True if an empty queue should be considered the end of the stream.
   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
   *     {@link C#RESULT_BUFFER_READ}.
   * @throws InsufficientCapacityException If the {@code buffer} has insufficient capacity to hold
   *     the data of a sample being read. The buffer {@link DecoderInputBuffer#timeUs timestamp} and
   *     flags are populated if this exception is thrown, but the read position is not advanced.
   */
  @CallSuper
  public int read(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      @ReadFlags int readFlags,
      boolean loadingFinished) {
    int result =
        peekSampleMetadata(
            formatHolder,
            buffer,
            /* formatRequired= */ (readFlags & FLAG_REQUIRE_FORMAT) != 0,
            loadingFinished,
            extrasHolder);
    if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
      boolean peek = (readFlags & FLAG_PEEK) != 0;
      if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
        if (peek) {
          sampleDataQueue.peekToBuffer(buffer, extrasHolder);
        } else {
          sampleDataQueue.readToBuffer(buffer, extrasHolder);
        }
      }
      if (!peek) {
        readPosition++;
      }
    }
    return result;
  }

  /**
   * Attempts to seek the read position to the specified sample index.
   *
   * @param sampleIndex The sample index.
   * @return Whether the seek was successful.
   */
  public final synchronized boolean seekTo(int sampleIndex) {
    rewind();
    if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) {
      return false;
    }
    startTimeUs = Long.MIN_VALUE;
    readPosition = sampleIndex - absoluteFirstIndex;
    return true;
  }

  /**
   * Attempts to seek the read position to the keyframe before or at the specified time.
   *
   * @param timeUs The time to seek to.
   * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
   *     end of the queue, by seeking to the last sample (or keyframe).
   * @return Whether the seek was successful.
   */
  public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer) {
    rewind();
    int relativeReadIndex = getRelativeIndex(readPosition);
    if (!hasNextSample()
        || timeUs < timesUs[relativeReadIndex]
        || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
      return false;
    }
    int offset =
        findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true);
    if (offset == -1) {
      return false;
    }
    startTimeUs = timeUs;
    readPosition += offset;
    return true;
  }

  /**
   * Returns the number of samples that need to be {@link #skip(int) skipped} to advance the read
   * position to the keyframe before or at the specified time.
   *
   * @param timeUs The time to advance to.
   * @param allowEndOfQueue Whether the end of the queue is considered a keyframe when {@code
   *     timeUs} is larger than the largest queued timestamp.
   * @return The number of samples that need to be skipped, which may be equal to 0.
   */
  public final synchronized int getSkipCount(long timeUs, boolean allowEndOfQueue) {
    int relativeReadIndex = getRelativeIndex(readPosition);
    if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) {
      return 0;
    }
    if (timeUs > largestQueuedTimestampUs && allowEndOfQueue) {
      return length - readPosition;
    }
    int offset =
        findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true);
    if (offset == -1) {
      return 0;
    }
    return offset;
  }

  /**
   * Advances the read position by the specified number of samples.
   *
   * @param count The number of samples to advance the read position by. Must be at least 0 and at
   *     most {@link #getWriteIndex()} - {@link #getReadIndex()}.
   */
  public final synchronized void skip(int count) {
    checkArgument(count >= 0 && readPosition + count <= length);
    readPosition += count;
  }

  /**
   * Discards up to but not including the sample immediately before or at the specified time.
   *
   * @param timeUs The time to discard up to.
   * @param toKeyframe If true then discards samples up to the keyframe before or at the specified
   *     time, rather than any sample before or at that time.
   * @param stopAtReadPosition If true then samples are only discarded if they're before the read
   *     position. If false then samples at and beyond the read position may be discarded, in which
   *     case the read position is advanced to the first remaining sample.
   */
  public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
    sampleDataQueue.discardDownstreamTo(
        discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition));
  }

  /** Discards up to but not including the read position. */
  public final void discardToRead() {
    sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead());
  }

  /** Discards all samples in the queue and advances the read position. */
  public final void discardToEnd() {
    sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd());
  }

  // Called by the loading thread.

  /**
   * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that
   * are subsequently queued.
   *
   * @param sampleOffsetUs The timestamp offset in microseconds.
   */
  public final void setSampleOffsetUs(long sampleOffsetUs) {
    if (this.sampleOffsetUs != sampleOffsetUs) {
      this.sampleOffsetUs = sampleOffsetUs;
      invalidateUpstreamFormatAdjustment();
    }
  }

  /**
   * Sets a listener to be notified of changes to the upstream format.
   *
   * @param listener The listener.
   */
  public final void setUpstreamFormatChangeListener(
      @Nullable UpstreamFormatChangedListener listener) {
    upstreamFormatChangeListener = listener;
  }

  // TrackOutput implementation. Called by the loading thread.

  @Override
  public final void format(Format unadjustedUpstreamFormat) {
    Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat);
    upstreamFormatAdjustmentRequired = false;
    this.unadjustedUpstreamFormat = unadjustedUpstreamFormat;
    boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
    if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
      upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
    }
  }

  @Override
  public final int sampleData(
      DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart)
      throws IOException {
    return sampleDataQueue.sampleData(input, length, allowEndOfInput);
  }

  @Override
  public final void sampleData(
      ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) {
    sampleDataQueue.sampleData(data, length);
  }

  @Override
  public void sampleMetadata(
      long timeUs,
      @C.BufferFlags int flags,
      int size,
      int offset,
      @Nullable CryptoData cryptoData) {
    if (upstreamFormatAdjustmentRequired) {
      format(Assertions.checkStateNotNull(unadjustedUpstreamFormat));
    }

    boolean isKeyframe = (flags & C.BUFFER_FLAG_KEY_FRAME) != 0;
    if (upstreamKeyframeRequired) {
      if (!isKeyframe) {
        return;
      }
      upstreamKeyframeRequired = false;
    }

    timeUs += sampleOffsetUs;
    if (upstreamAllSamplesAreSyncSamples) {
      if (timeUs < startTimeUs) {
        // If we know that all samples are sync samples, we can discard those that come before the
        // start time on the write side of the queue.
        return;
      }
      if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
        // The flag should always be set unless the source content has incorrect sample metadata.
        // Log a warning (once per format change, to avoid log spam) and override the flag.
        if (!loggedUnexpectedNonSyncSample) {
          Log.w(TAG, "Overriding unexpected non-sync sample for format: " + upstreamFormat);
          loggedUnexpectedNonSyncSample = true;
        }
        flags |= C.BUFFER_FLAG_KEY_FRAME;
      }
    }
    if (pendingSplice) {
      if (!isKeyframe || !attemptSplice(timeUs)) {
        return;
      }
      pendingSplice = false;
    }

    long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset;
    commitSample(timeUs, flags, absoluteOffset, size, cryptoData);
  }

  /**
   * Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)}
   * will be called to adjust the upstream {@link Format} again before the next sample is queued.
   */
  protected final void invalidateUpstreamFormatAdjustment() {
    upstreamFormatAdjustmentRequired = true;
  }

  /**
   * Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to
   * {@link #format(Format)}).
   *
   * <p>The default implementation incorporates the sample offset passed to {@link
   * #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}.
   *
   * @param format The {@link Format} to adjust.
   * @return The adjusted {@link Format}.
   */
  @CallSuper
  protected Format getAdjustedUpstreamFormat(Format format) {
    if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
      format =
          format
              .buildUpon()
              .setSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs)
              .build();
    }
    return format;
  }

  // Internal methods.

  /** Rewinds the read position to the first sample in the queue. */
  private synchronized void rewind() {
    readPosition = 0;
    sampleDataQueue.rewind();
  }

  @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat
  private synchronized int peekSampleMetadata(
      FormatHolder formatHolder,
      DecoderInputBuffer buffer,
      boolean formatRequired,
      boolean loadingFinished,
      SampleExtrasHolder extrasHolder) {
    buffer.waitingForKeys = false;
    if (!hasNextSample()) {
      if (loadingFinished || isLastSampleQueued) {
        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
        return C.RESULT_BUFFER_READ;
      } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) {
        onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder);
        return C.RESULT_FORMAT_READ;
      } else {
        return C.RESULT_NOTHING_READ;
      }
    }

    Format format = sharedSampleMetadata.get(getReadIndex()).format;
    if (formatRequired || format != downstreamFormat) {
      onFormatResult(format, formatHolder);
      return C.RESULT_FORMAT_READ;
    }

    int relativeReadIndex = getRelativeIndex(readPosition);
    if (!mayReadSample(relativeReadIndex)) {
      buffer.waitingForKeys = true;
      return C.RESULT_NOTHING_READ;
    }

    buffer.setFlags(flags[relativeReadIndex]);
    buffer.timeUs = timesUs[relativeReadIndex];
    if (buffer.timeUs < startTimeUs) {
      buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
    }
    extrasHolder.size = sizes[relativeReadIndex];
    extrasHolder.offset = offsets[relativeReadIndex];
    extrasHolder.cryptoData = cryptoDatas[relativeReadIndex];

    return C.RESULT_BUFFER_READ;
  }

  private synchronized boolean setUpstreamFormat(Format format) {
    upstreamFormatRequired = false;
    if (Util.areEqual(format, upstreamFormat)) {
      // The format is unchanged. If format and upstreamFormat are different objects, we keep the
      // current upstreamFormat so we can detect format changes on the read side using cheap
      // referential quality.
      return false;
    }

    if (!sharedSampleMetadata.isEmpty()
        && sharedSampleMetadata.getEndValue().format.equals(format)) {
      // The format has changed back to the format of the last committed sample. If they are
      // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat
      // so we can detect format changes on the read side using cheap referential equality.
      upstreamFormat = sharedSampleMetadata.getEndValue().format;
    } else {
      upstreamFormat = format;
    }
    upstreamAllSamplesAreSyncSamples =
        MimeTypes.allSamplesAreSyncSamples(upstreamFormat.sampleMimeType, upstreamFormat.codecs);
    loggedUnexpectedNonSyncSample = false;
    return true;
  }

  private synchronized long discardSampleMetadataTo(
      long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
    if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {
      return C.POSITION_UNSET;
    }
    int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
    int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);
    if (discardCount == -1) {
      return C.POSITION_UNSET;
    }
    return discardSamples(discardCount);
  }

  public synchronized long discardSampleMetadataToRead() {
    if (readPosition == 0) {
      return C.POSITION_UNSET;
    }
    return discardSamples(readPosition);
  }

  private synchronized long discardSampleMetadataToEnd() {
    if (length == 0) {
      return C.POSITION_UNSET;
    }
    return discardSamples(length);
  }

  private void releaseDrmSessionReferences() {
    if (currentDrmSession != null) {
      currentDrmSession.release(drmEventDispatcher);
      currentDrmSession = null;
      // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData
      // != null implies currentSession != null
      downstreamFormat = null;
    }
  }

  private synchronized void commitSample(
      long timeUs,
      @C.BufferFlags int sampleFlags,
      long offset,
      int size,
      @Nullable CryptoData cryptoData) {
    if (length > 0) {
      // Ensure sample data doesn't overlap.
      int previousSampleRelativeIndex = getRelativeIndex(length - 1);
      checkArgument(
          offsets[previousSampleRelativeIndex] + sizes[previousSampleRelativeIndex] <= offset);
    }

    isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0;
    largestQueuedTimestampUs = max(largestQueuedTimestampUs, timeUs);

    int relativeEndIndex = getRelativeIndex(length);
    timesUs[relativeEndIndex] = timeUs;
    offsets[relativeEndIndex] = offset;
    sizes[relativeEndIndex] = size;
    flags[relativeEndIndex] = sampleFlags;
    cryptoDatas[relativeEndIndex] = cryptoData;
    sourceIds[relativeEndIndex] = upstreamSourceId;

    if (sharedSampleMetadata.isEmpty()
        || !sharedSampleMetadata.getEndValue().format.equals(upstreamFormat)) {
      DrmSessionReference drmSessionReference =
          drmSessionManager != null
              ? drmSessionManager.preacquireSession(drmEventDispatcher, upstreamFormat)
              : DrmSessionReference.EMPTY;

      sharedSampleMetadata.appendSpan(
          getWriteIndex(),
          new SharedSampleMetadata(checkNotNull(upstreamFormat), drmSessionReference));
    }

    length++;
    if (length == capacity) {
      // Increase the capacity.
      int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
      int[] newSourceIds = new int[newCapacity];
      long[] newOffsets = new long[newCapacity];
      long[] newTimesUs = new long[newCapacity];
      int[] newFlags = new int[newCapacity];
      int[] newSizes = new int[newCapacity];
      CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
      int beforeWrap = capacity - relativeFirstIndex;
      System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);
      System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);
      System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);
      System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);
      System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);
      System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);
      int afterWrap = relativeFirstIndex;
      System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
      System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
      System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
      System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
      System.arraycopy(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap);
      System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
      offsets = newOffsets;
      timesUs = newTimesUs;
      flags = newFlags;
      sizes = newSizes;
      cryptoDatas = newCryptoDatas;
      sourceIds = newSourceIds;
      relativeFirstIndex = 0;
      capacity = newCapacity;
    }
  }

  /**
   * Attempts to discard samples from the end of the queue to allow samples starting from the
   * specified timestamp to be spliced in. Samples will not be discarded prior to the read position.
   *
   * @param timeUs The timestamp at which the splice occurs.
   * @return Whether the splice was successful.
   */
  private synchronized boolean attemptSplice(long timeUs) {
    if (length == 0) {
      return timeUs > largestDiscardedTimestampUs;
    }
    if (getLargestReadTimestampUs() >= timeUs) {
      return false;
    }
    int retainCount = countUnreadSamplesBefore(timeUs);
    discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount);
    return true;
  }

  private long discardUpstreamSampleMetadata(int discardFromIndex) {
    int discardCount = getWriteIndex() - discardFromIndex;
    checkArgument(0 <= discardCount && discardCount <= (length - readPosition));
    length -= discardCount;
    largestQueuedTimestampUs = max(largestDiscardedTimestampUs, getLargestTimestamp(length));
    isLastSampleQueued = discardCount == 0 && isLastSampleQueued;
    sharedSampleMetadata.discardFrom(discardFromIndex);
    if (length != 0) {
      int relativeLastWriteIndex = getRelativeIndex(length - 1);
      return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex];
    }
    return 0;
  }

  private boolean hasNextSample() {
    return readPosition != length;
  }

  /**
   * Sets the downstream format, performs DRM resource management, and populates the {@code
   * outputFormatHolder}.
   *
   * @param newFormat The new downstream format.
   * @param outputFormatHolder The output {@link FormatHolder}.
   */
  private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) {
    boolean isFirstFormat = downstreamFormat == null;
    @Nullable DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData;
    downstreamFormat = newFormat;
    @Nullable DrmInitData newDrmInitData = newFormat.drmInitData;

    outputFormatHolder.format =
        drmSessionManager != null
            ? newFormat.copyWithCryptoType(drmSessionManager.getCryptoType(newFormat))
            : newFormat;
    outputFormatHolder.drmSession = currentDrmSession;
    if (drmSessionManager == null) {
      // This sample queue is not expected to handle DRM. Nothing to do.
      return;
    }
    if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) {
      // Nothing to do.
      return;
    }
    // Ensure we acquire the new session before releasing the previous one in case the same session
    // is being used for both DrmInitData.
    @Nullable DrmSession previousSession = currentDrmSession;
    currentDrmSession = drmSessionManager.acquireSession(drmEventDispatcher, newFormat);
    outputFormatHolder.drmSession = currentDrmSession;

    if (previousSession != null) {
      previousSession.release(drmEventDispatcher);
    }
  }

  /**
   * Returns whether it's possible to read the next sample.
   *
   * @param relativeReadIndex The relative read index of the next sample.
   * @return Whether it's possible to read the next sample.
   */
  private boolean mayReadSample(int relativeReadIndex) {
    return currentDrmSession == null
        || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS
        || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0
            && currentDrmSession.playClearSamplesWithoutKeys());
  }

  /**
   * Finds the sample in the specified range that's before or at the specified time. If {@code
   * keyframe} is {@code true} then the sample is additionally required to be a keyframe.
   *
   * @param relativeStartIndex The relative index from which to start searching.
   * @param length The length of the range being searched.
   * @param timeUs The specified time.
   * @param keyframe Whether only keyframes should be considered.
   * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching
   *     sample was found.
   */
  private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {
    // This could be optimized to use a binary search, however in practice callers to this method
    // normally pass times near to the start of the search region. Hence it's unclear whether
    // switching to a binary search would yield any real benefit.
    int sampleCountToTarget = -1;
    int searchIndex = relativeStartIndex;
    for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) {
      if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
        // We've found a suitable sample.
        sampleCountToTarget = i;
        if (timesUs[searchIndex] == timeUs) {
          // Stop the search if we found a sample at the specified time to avoid returning a later
          // sample with the same exactly matching timestamp.
          break;
        }
      }
      searchIndex++;
      if (searchIndex == capacity) {
        searchIndex = 0;
      }
    }
    return sampleCountToTarget;
  }

  /**
   * Counts the number of samples that haven't been read that have a timestamp smaller than {@code
   * timeUs}.
   *
   * @param timeUs The specified time.
   * @return The number of unread samples with a timestamp smaller than {@code timeUs}.
   */
  private int countUnreadSamplesBefore(long timeUs) {
    int count = length;
    int relativeSampleIndex = getRelativeIndex(length - 1);
    while (count > readPosition && timesUs[relativeSampleIndex] >= timeUs) {
      count--;
      relativeSampleIndex--;
      if (relativeSampleIndex == -1) {
        relativeSampleIndex = capacity - 1;
      }
    }
    return count;
  }

  /**
   * Discards the specified number of samples.
   *
   * @param discardCount The number of samples to discard.
   * @return The corresponding offset up to which data should be discarded.
   */
  @GuardedBy("this")
  private long discardSamples(int discardCount) {
    largestDiscardedTimestampUs =
        max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount));
    length -= discardCount;
    absoluteFirstIndex += discardCount;
    relativeFirstIndex += discardCount;
    if (relativeFirstIndex >= capacity) {
      relativeFirstIndex -= capacity;
    }
    readPosition -= discardCount;
    if (readPosition < 0) {
      readPosition = 0;
    }
    sharedSampleMetadata.discardTo(absoluteFirstIndex);

    if (length == 0) {
      int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
      return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
    } else {
      return offsets[relativeFirstIndex];
    }
  }

  /**
   * Finds the largest timestamp of any sample from the start of the queue up to the specified
   * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of
   * the keyframe itself, and of subsequent frames.
   *
   * @param length The length of the range being searched.
   * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}.
   */
  private long getLargestTimestamp(int length) {
    if (length == 0) {
      return Long.MIN_VALUE;
    }
    long largestTimestampUs = Long.MIN_VALUE;
    int relativeSampleIndex = getRelativeIndex(length - 1);
    for (int i = 0; i < length; i++) {
      largestTimestampUs = max(largestTimestampUs, timesUs[relativeSampleIndex]);
      if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
        break;
      }
      relativeSampleIndex--;
      if (relativeSampleIndex == -1) {
        relativeSampleIndex = capacity - 1;
      }
    }
    return largestTimestampUs;
  }

  /**
   * Returns the relative index for a given offset from the start of the queue.
   *
   * @param offset The offset, which must be in the range [0, length].
   */
  private int getRelativeIndex(int offset) {
    int relativeIndex = relativeFirstIndex + offset;
    return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
  }

  /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */
  /* package */ static final class SampleExtrasHolder {

    public int size;
    public long offset;
    @Nullable public CryptoData cryptoData;
  }

  /** A holder for metadata that applies to a span of contiguous samples. */
  private static final class SharedSampleMetadata {
    public final Format format;
    public final DrmSessionReference drmSessionReference;

    private SharedSampleMetadata(Format format, DrmSessionReference drmSessionReference) {
      this.format = format;
      this.drmSessionReference = drmSessionReference;
    }
  }
}