public class

DefaultDashChunkSource

extends java.lang.Object

implements DashChunkSource

 java.lang.Object

↳androidx.media3.exoplayer.dash.DefaultDashChunkSource

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer-dash', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer-dash
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer-dash:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A default DashChunkSource implementation.

Summary

Fields
protected final DefaultDashChunkSource.RepresentationHolderrepresentationHolders

Constructors
publicDefaultDashChunkSource(ChunkExtractor.Factory chunkExtractorFactory, LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, BaseUrlExclusionList baseUrlExclusionList, int periodIndex, int[] adaptationSetIndices[], ExoTrackSelection trackSelection, int trackType, DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, java.util.List<Format> closedCaptionFormats, PlayerEmsgHandler.PlayerTrackEmsgHandler playerTrackEmsgHandler, PlayerId playerId, CmcdConfiguration cmcdConfiguration)

Methods
public longgetAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters)

public voidgetNextChunk(LoadingInfo loadingInfo, long loadPositionUs, java.util.List<MediaChunk> queue, ChunkHolder out)

public intgetPreferredQueueSize(long playbackPositionUs, java.util.List<MediaChunk> queue)

public voidmaybeThrowError()

protected ChunknewInitializationChunk(DefaultDashChunkSource.RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, java.lang.Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri, CmcdData.Factory cmcdDataFactory)

Creates a new Chunk for initialization.

protected ChunknewMediaChunk(DefaultDashChunkSource.RepresentationHolder representationHolder, DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, java.lang.Object trackSelectionData, long firstSegmentNum, int maxSegmentCount, long seekTimeUs, long nowPeriodTimeUs, CmcdData.Factory cmcdDataFactory)

public voidonChunkLoadCompleted(Chunk chunk)

public booleanonChunkLoadError(Chunk chunk, boolean cancelable, LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, LoadErrorHandlingPolicy loadErrorHandlingPolicy)

public voidrelease()

public booleanshouldCancelLoad(long playbackPositionUs, Chunk loadingChunk, java.util.List<MediaChunk> queue)

public voidupdateManifest(DashManifest newManifest, int newPeriodIndex)

public voidupdateTrackSelection(ExoTrackSelection trackSelection)

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

Fields

protected final DefaultDashChunkSource.RepresentationHolder representationHolders

Constructors

public DefaultDashChunkSource(ChunkExtractor.Factory chunkExtractorFactory, LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, BaseUrlExclusionList baseUrlExclusionList, int periodIndex, int[] adaptationSetIndices[], ExoTrackSelection trackSelection, int trackType, DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, java.util.List<Format> closedCaptionFormats, PlayerEmsgHandler.PlayerTrackEmsgHandler playerTrackEmsgHandler, PlayerId playerId, CmcdConfiguration cmcdConfiguration)

Parameters:

chunkExtractorFactory: Creates ChunkExtractor instances to use for extracting chunks.
manifestLoaderErrorThrower: Throws errors affecting loading of manifests.
manifest: The initial manifest.
baseUrlExclusionList: The base URL exclusion list.
periodIndex: The index of the period in the manifest.
adaptationSetIndices: The indices of the adaptation sets in the period.
trackSelection: The track selection.
trackType: The of the tracks in the selection.
dataSource: A DataSource suitable for loading the media data.
elapsedRealtimeOffsetMs: If known, an estimate of the instantaneous difference between server-side unix time and in milliseconds, specified as the server's unix time minus the local elapsed time. Or C.TIME_UNSET if unknown.
maxSegmentsPerLoad: The maximum number of segments to combine into a single request. Note that segments will only be combined if their s are the same and if their data ranges are adjacent.
enableEventMessageTrack: Whether to output an event message track.
closedCaptionFormats: The Formats of closed caption tracks to be output.
playerTrackEmsgHandler: The PlayerEmsgHandler.PlayerTrackEmsgHandler instance to handle emsg messages targeting the player. Maybe null if this is not necessary.
playerId: The PlayerId of the player using this chunk source.
cmcdConfiguration: The CmcdConfiguration for this chunk source.

Methods

public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters)

public void updateManifest(DashManifest newManifest, int newPeriodIndex)

public void updateTrackSelection(ExoTrackSelection trackSelection)

public void maybeThrowError()

public int getPreferredQueueSize(long playbackPositionUs, java.util.List<MediaChunk> queue)

public boolean shouldCancelLoad(long playbackPositionUs, Chunk loadingChunk, java.util.List<MediaChunk> queue)

public void getNextChunk(LoadingInfo loadingInfo, long loadPositionUs, java.util.List<MediaChunk> queue, ChunkHolder out)

public void onChunkLoadCompleted(Chunk chunk)

public boolean onChunkLoadError(Chunk chunk, boolean cancelable, LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, LoadErrorHandlingPolicy loadErrorHandlingPolicy)

public void release()

protected Chunk newInitializationChunk(DefaultDashChunkSource.RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, java.lang.Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri, CmcdData.Factory cmcdDataFactory)

Creates a new Chunk for initialization.

Parameters:

representationHolder: The Representation holder for initialization.
dataSource: The source from which the data should be loaded.
trackFormat: The format of the track to which this chunk belongs.
trackSelectionReason: One of the .
trackSelectionData: Additional data related to track selection.
initializationUri: The URI pointing to initialization data. Can be null if indexUri is not null.
indexUri: The URI pointing to index data. Can be null if initializationUri is not null.
cmcdDataFactory: The for generating CMCD data.

protected Chunk newMediaChunk(DefaultDashChunkSource.RepresentationHolder representationHolder, DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, java.lang.Object trackSelectionData, long firstSegmentNum, int maxSegmentCount, long seekTimeUs, long nowPeriodTimeUs, CmcdData.Factory cmcdDataFactory)

Source

/*
 * Copyright (C) 2016 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.dash;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;

import android.net.Uri;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.UriUtil;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.HttpDataSource.InvalidResponseCodeException;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.LoadingInfo;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler;
import androidx.media3.exoplayer.dash.manifest.AdaptationSet;
import androidx.media3.exoplayer.dash.manifest.BaseUrl;
import androidx.media3.exoplayer.dash.manifest.DashManifest;
import androidx.media3.exoplayer.dash.manifest.RangedUri;
import androidx.media3.exoplayer.dash.manifest.Representation;
import androidx.media3.exoplayer.source.BehindLiveWindowException;
import androidx.media3.exoplayer.source.chunk.BaseMediaChunkIterator;
import androidx.media3.exoplayer.source.chunk.BundledChunkExtractor;
import androidx.media3.exoplayer.source.chunk.Chunk;
import androidx.media3.exoplayer.source.chunk.ChunkExtractor;
import androidx.media3.exoplayer.source.chunk.ChunkHolder;
import androidx.media3.exoplayer.source.chunk.ContainerMediaChunk;
import androidx.media3.exoplayer.source.chunk.InitializationChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunk;
import androidx.media3.exoplayer.source.chunk.MediaChunkIterator;
import androidx.media3.exoplayer.source.chunk.SingleSampleMediaChunk;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.CmcdConfiguration;
import androidx.media3.exoplayer.upstream.CmcdData;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoaderErrorThrower;
import androidx.media3.extractor.ChunkIndex;
import androidx.media3.extractor.text.SubtitleParser;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/** A default {@link DashChunkSource} implementation. */
@UnstableApi
public class DefaultDashChunkSource implements DashChunkSource {

  /** {@link DashChunkSource.Factory} for {@link DefaultDashChunkSource} instances. */
  public static final class Factory implements DashChunkSource.Factory {

    private final DataSource.Factory dataSourceFactory;
    private final int maxSegmentsPerLoad;
    private final ChunkExtractor.Factory chunkExtractorFactory;

    /**
     * Equivalent to {@link #Factory(ChunkExtractor.Factory, DataSource.Factory, int) new
     * Factory(BundledChunkExtractor.FACTORY, dataSourceFactory, maxSegmentsPerLoad = 1)}.
     */
    public Factory(DataSource.Factory dataSourceFactory) {
      this(dataSourceFactory, /* maxSegmentsPerLoad= */ 1);
    }

    /**
     * Equivalent to {@link #Factory(ChunkExtractor.Factory, DataSource.Factory, int) new
     * Factory(BundledChunkExtractor.FACTORY, dataSourceFactory, maxSegmentsPerLoad)}.
     */
    public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) {
      this(BundledChunkExtractor.FACTORY, dataSourceFactory, maxSegmentsPerLoad);
    }

    /**
     * Creates a new instance.
     *
     * @param chunkExtractorFactory Creates {@link ChunkExtractor} instances to use for extracting
     *     chunks.
     * @param dataSourceFactory Creates the {@link DataSource} to use for downloading chunks.
     * @param maxSegmentsPerLoad See {@link DefaultDashChunkSource#DefaultDashChunkSource}.
     */
    public Factory(
        ChunkExtractor.Factory chunkExtractorFactory,
        DataSource.Factory dataSourceFactory,
        int maxSegmentsPerLoad) {
      this.chunkExtractorFactory = chunkExtractorFactory;
      this.dataSourceFactory = dataSourceFactory;
      this.maxSegmentsPerLoad = maxSegmentsPerLoad;
    }

    @CanIgnoreReturnValue
    @Override
    public Factory setSubtitleParserFactory(SubtitleParser.Factory subtitleParserFactory) {
      chunkExtractorFactory.setSubtitleParserFactory(subtitleParserFactory);
      return this;
    }

    @CanIgnoreReturnValue
    @Override
    public Factory experimentalParseSubtitlesDuringExtraction(
        boolean parseSubtitlesDuringExtraction) {
      chunkExtractorFactory.experimentalParseSubtitlesDuringExtraction(
          parseSubtitlesDuringExtraction);
      return this;
    }

    @Override
    public DashChunkSource createDashChunkSource(
        LoaderErrorThrower manifestLoaderErrorThrower,
        DashManifest manifest,
        BaseUrlExclusionList baseUrlExclusionList,
        int periodIndex,
        int[] adaptationSetIndices,
        ExoTrackSelection trackSelection,
        @C.TrackType int trackType,
        long elapsedRealtimeOffsetMs,
        boolean enableEventMessageTrack,
        List<Format> closedCaptionFormats,
        @Nullable PlayerTrackEmsgHandler playerEmsgHandler,
        @Nullable TransferListener transferListener,
        PlayerId playerId,
        @Nullable CmcdConfiguration cmcdConfiguration) {
      DataSource dataSource = dataSourceFactory.createDataSource();
      if (transferListener != null) {
        dataSource.addTransferListener(transferListener);
      }
      return new DefaultDashChunkSource(
          chunkExtractorFactory,
          manifestLoaderErrorThrower,
          manifest,
          baseUrlExclusionList,
          periodIndex,
          adaptationSetIndices,
          trackSelection,
          trackType,
          dataSource,
          elapsedRealtimeOffsetMs,
          maxSegmentsPerLoad,
          enableEventMessageTrack,
          closedCaptionFormats,
          playerEmsgHandler,
          playerId,
          cmcdConfiguration);
    }

    /**
     * {@inheritDoc}
     *
     * <p>This implementation delegates determining of the output format to the {@link
     * ChunkExtractor.Factory} passed to the constructor of this class.
     */
    @Override
    public Format getOutputTextFormat(Format sourceFormat) {
      return chunkExtractorFactory.getOutputTextFormat(sourceFormat);
    }
  }

  private final LoaderErrorThrower manifestLoaderErrorThrower;
  private final BaseUrlExclusionList baseUrlExclusionList;
  private final int[] adaptationSetIndices;
  private final @C.TrackType int trackType;
  private final DataSource dataSource;
  private final long elapsedRealtimeOffsetMs;
  private final int maxSegmentsPerLoad;
  @Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler;
  @Nullable private final CmcdConfiguration cmcdConfiguration;

  protected final RepresentationHolder[] representationHolders;

  private ExoTrackSelection trackSelection;
  private DashManifest manifest;
  private int periodIndex;
  @Nullable private IOException fatalError;
  private boolean missingLastSegment;

  /**
   * The time at which the last {@link #getNextChunk(LoadingInfo, long, List, ChunkHolder)} method
   * was called, as measured by {@link SystemClock#elapsedRealtime}.
   */
  private long lastChunkRequestRealtimeMs;

  /**
   * @param chunkExtractorFactory Creates {@link ChunkExtractor} instances to use for extracting
   *     chunks.
   * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
   * @param manifest The initial manifest.
   * @param baseUrlExclusionList The base URL exclusion list.
   * @param periodIndex The index of the period in the manifest.
   * @param adaptationSetIndices The indices of the adaptation sets in the period.
   * @param trackSelection The track selection.
   * @param trackType The {@link C.TrackType type} of the tracks in the selection.
   * @param dataSource A {@link DataSource} suitable for loading the media data.
   * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
   *     server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
   *     as the server's unix time minus the local elapsed time. Or {@link C#TIME_UNSET} if unknown.
   * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note
   *     that segments will only be combined if their {@link Uri}s are the same and if their data
   *     ranges are adjacent.
   * @param enableEventMessageTrack Whether to output an event message track.
   * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output.
   * @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg
   *     messages targeting the player. Maybe null if this is not necessary.
   * @param playerId The {@link PlayerId} of the player using this chunk source.
   * @param cmcdConfiguration The {@link CmcdConfiguration} for this chunk source.
   */
  public DefaultDashChunkSource(
      ChunkExtractor.Factory chunkExtractorFactory,
      LoaderErrorThrower manifestLoaderErrorThrower,
      DashManifest manifest,
      BaseUrlExclusionList baseUrlExclusionList,
      int periodIndex,
      int[] adaptationSetIndices,
      ExoTrackSelection trackSelection,
      @C.TrackType int trackType,
      DataSource dataSource,
      long elapsedRealtimeOffsetMs,
      int maxSegmentsPerLoad,
      boolean enableEventMessageTrack,
      List<Format> closedCaptionFormats,
      @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler,
      PlayerId playerId,
      @Nullable CmcdConfiguration cmcdConfiguration) {
    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
    this.manifest = manifest;
    this.baseUrlExclusionList = baseUrlExclusionList;
    this.adaptationSetIndices = adaptationSetIndices;
    this.trackSelection = trackSelection;
    this.trackType = trackType;
    this.dataSource = dataSource;
    this.periodIndex = periodIndex;
    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
    this.maxSegmentsPerLoad = maxSegmentsPerLoad;
    this.playerTrackEmsgHandler = playerTrackEmsgHandler;
    this.cmcdConfiguration = cmcdConfiguration;
    this.lastChunkRequestRealtimeMs = C.TIME_UNSET;

    long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);

    List<Representation> representations = getRepresentations();
    representationHolders = new RepresentationHolder[trackSelection.length()];
    for (int i = 0; i < representationHolders.length; i++) {
      Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
      @Nullable
      BaseUrl selectedBaseUrl = baseUrlExclusionList.selectBaseUrl(representation.baseUrls);
      representationHolders[i] =
          new RepresentationHolder(
              periodDurationUs,
              representation,
              selectedBaseUrl != null ? selectedBaseUrl : representation.baseUrls.get(0),
              chunkExtractorFactory.createProgressiveMediaExtractor(
                  trackType,
                  representation.format,
                  enableEventMessageTrack,
                  closedCaptionFormats,
                  playerTrackEmsgHandler,
                  playerId),
              /* segmentNumShift= */ 0,
              representation.getIndex());
    }
  }

  @Override
  public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
    // Segments are aligned across representations, so any segment index will do.
    for (RepresentationHolder representationHolder : representationHolders) {
      if (representationHolder.segmentIndex != null) {
        long segmentCount = representationHolder.getSegmentCount();
        if (segmentCount == 0) {
          continue;
        }
        long segmentNum = representationHolder.getSegmentNum(positionUs);
        long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);
        long secondSyncUs =
            firstSyncUs < positionUs
                    && (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED
                        || segmentNum
                            < representationHolder.getFirstSegmentNum() + segmentCount - 1)
                ? representationHolder.getSegmentStartTimeUs(segmentNum + 1)
                : firstSyncUs;
        return seekParameters.resolveSeekPositionUs(positionUs, firstSyncUs, secondSyncUs);
      }
    }
    // We don't have a segment index to adjust the seek position with yet.
    return positionUs;
  }

  @Override
  public void updateManifest(DashManifest newManifest, int newPeriodIndex) {
    try {
      manifest = newManifest;
      periodIndex = newPeriodIndex;
      long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
      List<Representation> representations = getRepresentations();
      for (int i = 0; i < representationHolders.length; i++) {
        Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
        representationHolders[i] =
            representationHolders[i].copyWithNewRepresentation(periodDurationUs, representation);
      }
    } catch (BehindLiveWindowException e) {
      fatalError = e;
    }
  }

  @Override
  public void updateTrackSelection(ExoTrackSelection trackSelection) {
    this.trackSelection = trackSelection;
  }

  @Override
  public void maybeThrowError() throws IOException {
    if (fatalError != null) {
      throw fatalError;
    } else {
      manifestLoaderErrorThrower.maybeThrowError();
    }
  }

  @Override
  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
    if (fatalError != null || trackSelection.length() < 2) {
      return queue.size();
    }
    return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
  }

  @Override
  public boolean shouldCancelLoad(
      long playbackPositionUs, Chunk loadingChunk, List<? extends MediaChunk> queue) {
    if (fatalError != null) {
      return false;
    }
    return trackSelection.shouldCancelChunkLoad(playbackPositionUs, loadingChunk, queue);
  }

  @Override
  public void getNextChunk(
      LoadingInfo loadingInfo,
      long loadPositionUs,
      List<? extends MediaChunk> queue,
      ChunkHolder out) {
    if (fatalError != null) {
      return;
    }

    long playbackPositionUs = loadingInfo.playbackPositionUs;
    long bufferedDurationUs = loadPositionUs - playbackPositionUs;
    long presentationPositionUs =
        Util.msToUs(manifest.availabilityStartTimeMs)
            + Util.msToUs(manifest.getPeriod(periodIndex).startMs)
            + loadPositionUs;

    if (playerTrackEmsgHandler != null
        && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(
            presentationPositionUs)) {
      return;
    }

    long nowUnixTimeUs = Util.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs));
    long nowPeriodTimeUs = getNowPeriodTimeUs(nowUnixTimeUs);
    MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1);
    MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()];
    for (int i = 0; i < chunkIterators.length; i++) {
      RepresentationHolder representationHolder = representationHolders[i];
      if (representationHolder.segmentIndex == null) {
        chunkIterators[i] = MediaChunkIterator.EMPTY;
      } else {
        long firstAvailableSegmentNum =
            representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs);
        long lastAvailableSegmentNum =
            representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs);
        long segmentNum =
            getSegmentNum(
                representationHolder,
                previous,
                loadPositionUs,
                firstAvailableSegmentNum,
                lastAvailableSegmentNum);
        if (segmentNum < firstAvailableSegmentNum) {
          chunkIterators[i] = MediaChunkIterator.EMPTY;
        } else {
          representationHolder = updateSelectedBaseUrl(/* trackIndex= */ i);
          chunkIterators[i] =
              new RepresentationSegmentIterator(
                  representationHolder, segmentNum, lastAvailableSegmentNum, nowPeriodTimeUs);
        }
      }
    }

    long availableLiveDurationUs = getAvailableLiveDurationUs(nowUnixTimeUs, playbackPositionUs);
    trackSelection.updateSelectedTrack(
        playbackPositionUs, bufferedDurationUs, availableLiveDurationUs, queue, chunkIterators);
    int selectedTrackIndex = trackSelection.getSelectedIndex();

    @Nullable
    CmcdData.Factory cmcdDataFactory =
        cmcdConfiguration == null
            ? null
            : new CmcdData.Factory(
                cmcdConfiguration,
                trackSelection,
                max(0, bufferedDurationUs),
                /* playbackRate= */ loadingInfo.playbackSpeed,
                /* streamingFormat= */ CmcdData.Factory.STREAMING_FORMAT_DASH,
                /* isLive= */ manifest.dynamic,
                /* didRebuffer= */ loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs),
                /* isBufferEmpty= */ queue.isEmpty());
    lastChunkRequestRealtimeMs = SystemClock.elapsedRealtime();

    RepresentationHolder representationHolder = updateSelectedBaseUrl(selectedTrackIndex);
    if (representationHolder.chunkExtractor != null) {
      Representation selectedRepresentation = representationHolder.representation;
      @Nullable RangedUri pendingInitializationUri = null;
      @Nullable RangedUri pendingIndexUri = null;
      if (representationHolder.chunkExtractor.getSampleFormats() == null) {
        pendingInitializationUri = selectedRepresentation.getInitializationUri();
      }
      if (representationHolder.segmentIndex == null) {
        pendingIndexUri = selectedRepresentation.getIndexUri();
      }
      if (pendingInitializationUri != null || pendingIndexUri != null) {
        // We have initialization and/or index requests to make.
        out.chunk =
            newInitializationChunk(
                representationHolder,
                dataSource,
                trackSelection.getSelectedFormat(),
                trackSelection.getSelectionReason(),
                trackSelection.getSelectionData(),
                pendingInitializationUri,
                pendingIndexUri,
                cmcdDataFactory);
        return;
      }
    }

    long periodDurationUs = representationHolder.periodDurationUs;
    boolean isLastPeriodInDynamicManifest =
        manifest.dynamic && periodIndex == manifest.getPeriodCount() - 1;
    boolean periodEnded = !isLastPeriodInDynamicManifest || periodDurationUs != C.TIME_UNSET;

    if (representationHolder.getSegmentCount() == 0) {
      // The index doesn't define any segments.
      out.endOfStream = periodEnded;
      return;
    }

    long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs);
    long lastAvailableSegmentNum = representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs);
    if (isLastPeriodInDynamicManifest) {
      long lastAvailableSegmentEndTimeUs =
          representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum);
      long lastSegmentDurationUs =
          lastAvailableSegmentEndTimeUs
              - representationHolder.getSegmentStartTimeUs(lastAvailableSegmentNum);
      // Account for some inaccuracy in the overall period duration value by assuming that the
      // period is finished once no further full sample fits into the overall duration.
      periodEnded &= (lastAvailableSegmentEndTimeUs + lastSegmentDurationUs >= periodDurationUs);
    }
    long segmentNum =
        getSegmentNum(
            representationHolder,
            previous,
            loadPositionUs,
            firstAvailableSegmentNum,
            lastAvailableSegmentNum);
    if (segmentNum < firstAvailableSegmentNum) {
      // This is before the first chunk in the current manifest.
      fatalError = new BehindLiveWindowException();
      return;
    }

    if (segmentNum > lastAvailableSegmentNum
        || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
      // The segment is beyond the end of the period.
      out.endOfStream = periodEnded;
      return;
    }

    if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
      // The period duration clips the period to a position before the segment.
      out.endOfStream = true;
      return;
    }

    int maxSegmentCount = (int) min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
    if (periodDurationUs != C.TIME_UNSET) {
      while (maxSegmentCount > 1
          && representationHolder.getSegmentStartTimeUs(segmentNum + maxSegmentCount - 1)
              >= periodDurationUs) {
        // The period duration clips the period to a position before the last segment in the range
        // [segmentNum, segmentNum + maxSegmentCount - 1]. Reduce maxSegmentCount.
        maxSegmentCount--;
      }
    }

    long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;
    out.chunk =
        newMediaChunk(
            representationHolder,
            dataSource,
            trackType,
            trackSelection.getSelectedFormat(),
            trackSelection.getSelectionReason(),
            trackSelection.getSelectionData(),
            segmentNum,
            maxSegmentCount,
            seekTimeUs,
            nowPeriodTimeUs,
            cmcdDataFactory);
  }

  @Override
  public void onChunkLoadCompleted(Chunk chunk) {
    if (chunk instanceof InitializationChunk) {
      InitializationChunk initializationChunk = (InitializationChunk) chunk;
      int trackIndex = trackSelection.indexOf(initializationChunk.trackFormat);
      RepresentationHolder representationHolder = representationHolders[trackIndex];
      // The null check avoids overwriting an index obtained from the manifest with one obtained
      // from the stream. If the manifest defines an index then the stream shouldn't, but in cases
      // where it does we should ignore it.
      if (representationHolder.segmentIndex == null) {
        @Nullable
        ChunkIndex chunkIndex =
            checkStateNotNull(representationHolder.chunkExtractor).getChunkIndex();
        if (chunkIndex != null) {
          representationHolders[trackIndex] =
              representationHolder.copyWithNewSegmentIndex(
                  new DashWrappingSegmentIndex(
                      chunkIndex, representationHolder.representation.presentationTimeOffsetUs));
        }
      }
    }
    if (playerTrackEmsgHandler != null) {
      playerTrackEmsgHandler.onChunkLoadCompleted(chunk);
    }
  }

  @Override
  public boolean onChunkLoadError(
      Chunk chunk,
      boolean cancelable,
      LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo,
      LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
    if (!cancelable) {
      return false;
    }
    if (playerTrackEmsgHandler != null && playerTrackEmsgHandler.onChunkLoadError(chunk)) {
      return true;
    }
    // Workaround for missing segment at the end of the period
    if (!manifest.dynamic
        && chunk instanceof MediaChunk
        && loadErrorInfo.exception instanceof InvalidResponseCodeException
        && ((InvalidResponseCodeException) loadErrorInfo.exception).responseCode == 404) {
      RepresentationHolder representationHolder =
          representationHolders[trackSelection.indexOf(chunk.trackFormat)];
      long segmentCount = representationHolder.getSegmentCount();
      if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) {
        long lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1;
        if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {
          missingLastSegment = true;
          return true;
        }
      }
    }

    int trackIndex = trackSelection.indexOf(chunk.trackFormat);
    RepresentationHolder representationHolder = representationHolders[trackIndex];
    @Nullable
    BaseUrl newBaseUrl =
        baseUrlExclusionList.selectBaseUrl(representationHolder.representation.baseUrls);
    if (newBaseUrl != null && !representationHolder.selectedBaseUrl.equals(newBaseUrl)) {
      // The base URL has changed since the failing chunk was created. Request a replacement chunk,
      // which will use the new base URL.
      return true;
    }

    LoadErrorHandlingPolicy.FallbackOptions fallbackOptions =
        createFallbackOptions(trackSelection, representationHolder.representation.baseUrls);
    if (!fallbackOptions.isFallbackAvailable(LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK)
        && !fallbackOptions.isFallbackAvailable(LoadErrorHandlingPolicy.FALLBACK_TYPE_LOCATION)) {
      return false;
    }
    @Nullable
    LoadErrorHandlingPolicy.FallbackSelection fallbackSelection =
        loadErrorHandlingPolicy.getFallbackSelectionFor(fallbackOptions, loadErrorInfo);
    if (fallbackSelection == null || !fallbackOptions.isFallbackAvailable(fallbackSelection.type)) {
      // Policy indicated to not use any fallback or a fallback type that is not available.
      return false;
    }

    boolean cancelLoad = false;
    if (fallbackSelection.type == LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK) {
      cancelLoad =
          trackSelection.excludeTrack(
              trackSelection.indexOf(chunk.trackFormat), fallbackSelection.exclusionDurationMs);
    } else if (fallbackSelection.type == LoadErrorHandlingPolicy.FALLBACK_TYPE_LOCATION) {
      baseUrlExclusionList.exclude(
          representationHolder.selectedBaseUrl, fallbackSelection.exclusionDurationMs);
      cancelLoad = true;
    }
    return cancelLoad;
  }

  @Override
  public void release() {
    for (RepresentationHolder representationHolder : representationHolders) {
      @Nullable ChunkExtractor chunkExtractor = representationHolder.chunkExtractor;
      if (chunkExtractor != null) {
        chunkExtractor.release();
      }
    }
  }

  // Internal methods.

  private LoadErrorHandlingPolicy.FallbackOptions createFallbackOptions(
      ExoTrackSelection trackSelection, List<BaseUrl> baseUrls) {
    long nowMs = SystemClock.elapsedRealtime();
    int numberOfTracks = trackSelection.length();
    int numberOfExcludedTracks = 0;
    for (int i = 0; i < numberOfTracks; i++) {
      if (trackSelection.isTrackExcluded(i, nowMs)) {
        numberOfExcludedTracks++;
      }
    }
    int priorityCount = BaseUrlExclusionList.getPriorityCount(baseUrls);
    return new LoadErrorHandlingPolicy.FallbackOptions(
        /* numberOfLocations= */ priorityCount,
        /* numberOfExcludedLocations= */ priorityCount
            - baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls),
        numberOfTracks,
        numberOfExcludedTracks);
  }

  private long getSegmentNum(
      RepresentationHolder representationHolder,
      @Nullable MediaChunk previousChunk,
      long loadPositionUs,
      long firstAvailableSegmentNum,
      long lastAvailableSegmentNum) {
    return previousChunk != null
        ? previousChunk.getNextChunkIndex()
        : Util.constrainValue(
            representationHolder.getSegmentNum(loadPositionUs),
            firstAvailableSegmentNum,
            lastAvailableSegmentNum);
  }

  @RequiresNonNull({"manifest", "adaptationSetIndices"})
  private ArrayList<Representation> getRepresentations(
      @UnknownInitialization DefaultDashChunkSource this) {
    List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
    ArrayList<Representation> representations = new ArrayList<>();
    for (int adaptationSetIndex : adaptationSetIndices) {
      representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations);
    }
    return representations;
  }

  private long getAvailableLiveDurationUs(long nowUnixTimeUs, long playbackPositionUs) {
    if (!manifest.dynamic || representationHolders[0].getSegmentCount() == 0) {
      return C.TIME_UNSET;
    }
    long lastSegmentNum = representationHolders[0].getLastAvailableSegmentNum(nowUnixTimeUs);
    long lastSegmentEndTimeUs = representationHolders[0].getSegmentEndTimeUs(lastSegmentNum);
    long nowPeriodTimeUs = getNowPeriodTimeUs(nowUnixTimeUs);
    long availabilityEndTimeUs = min(nowPeriodTimeUs, lastSegmentEndTimeUs);
    return max(0, availabilityEndTimeUs - playbackPositionUs);
  }

  private long getNowPeriodTimeUs(long nowUnixTimeUs) {
    return manifest.availabilityStartTimeMs == C.TIME_UNSET
        ? C.TIME_UNSET
        : nowUnixTimeUs
            - Util.msToUs(
                manifest.availabilityStartTimeMs + manifest.getPeriod(periodIndex).startMs);
  }

  /**
   * Creates a new {@link Chunk} for initialization.
   *
   * @param representationHolder The {@link Representation} holder for initialization.
   * @param dataSource The source from which the data should be loaded.
   * @param trackFormat The format of the track to which this chunk belongs.
   * @param trackSelectionReason One of the {@link C.SelectionReason selection reasons}.
   * @param trackSelectionData Additional data related to track selection.
   * @param initializationUri The URI pointing to initialization data. Can be {@code null} if {@code
   *     indexUri} is not {@code null}.
   * @param indexUri The URI pointing to index data. Can be {@code null} if {@code
   *     initializationUri} is not {@code null}.
   * @param cmcdDataFactory The {@link CmcdData.Factory} for generating CMCD data.
   */
  @RequiresNonNull("#1.chunkExtractor")
  protected Chunk newInitializationChunk(
      RepresentationHolder representationHolder,
      DataSource dataSource,
      Format trackFormat,
      @C.SelectionReason int trackSelectionReason,
      @Nullable Object trackSelectionData,
      @Nullable RangedUri initializationUri,
      @Nullable RangedUri indexUri,
      @Nullable CmcdData.Factory cmcdDataFactory) {
    Representation representation = representationHolder.representation;
    RangedUri requestUri;
    if (initializationUri != null) {
      // It's common for initialization and index data to be stored adjacently. Attempt to merge
      // the two requests together to request both at once.
      requestUri =
          initializationUri.attemptMerge(indexUri, representationHolder.selectedBaseUrl.url);
      if (requestUri == null) {
        requestUri = initializationUri;
      }
    } else {
      requestUri = checkNotNull(indexUri);
    }
    DataSpec dataSpec =
        DashUtil.buildDataSpec(
            representation,
            representationHolder.selectedBaseUrl.url,
            requestUri,
            /* flags= */ 0,
            /* httpRequestHeaders= */ ImmutableMap.of());
    if (cmcdDataFactory != null) {
      CmcdData cmcdData =
          cmcdDataFactory.setObjectType(CmcdData.Factory.OBJECT_TYPE_INIT_SEGMENT).createCmcdData();
      dataSpec = cmcdData.addToDataSpec(dataSpec);
    }

    return new InitializationChunk(
        dataSource,
        dataSpec,
        trackFormat,
        trackSelectionReason,
        trackSelectionData,
        representationHolder.chunkExtractor);
  }

  // TODO: b/289983417 - Stop using deprecated SingleSampleMediaChunk when legacy subtitle decoding
  // is no longer supported.
  @SuppressWarnings("deprecation")
  protected Chunk newMediaChunk(
      RepresentationHolder representationHolder,
      DataSource dataSource,
      @C.TrackType int trackType,
      Format trackFormat,
      @C.SelectionReason int trackSelectionReason,
      @Nullable Object trackSelectionData,
      long firstSegmentNum,
      int maxSegmentCount,
      long seekTimeUs,
      long nowPeriodTimeUs,
      @Nullable CmcdData.Factory cmcdDataFactory) {
    Representation representation = representationHolder.representation;
    long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
    RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
    if (representationHolder.chunkExtractor == null) {
      long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);
      int flags =
          representationHolder.isSegmentAvailableAtFullNetworkSpeed(
                  firstSegmentNum, nowPeriodTimeUs)
              ? 0
              : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
      DataSpec dataSpec =
          DashUtil.buildDataSpec(
              representation,
              representationHolder.selectedBaseUrl.url,
              segmentUri,
              flags,
              /* httpRequestHeaders= */ ImmutableMap.of());
      if (cmcdDataFactory != null) {
        cmcdDataFactory
            .setChunkDurationUs(endTimeUs - startTimeUs)
            .setObjectType(CmcdData.Factory.getObjectType(trackSelection));
        @Nullable
        Pair<String, String> nextObjectAndRangeRequest =
            getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
        if (nextObjectAndRangeRequest != null) {
          cmcdDataFactory
              .setNextObjectRequest(nextObjectAndRangeRequest.first)
              .setNextRangeRequest(nextObjectAndRangeRequest.second);
        }
        CmcdData cmcdData = cmcdDataFactory.createCmcdData();
        dataSpec = cmcdData.addToDataSpec(dataSpec);
      }

      return new SingleSampleMediaChunk(
          dataSource,
          dataSpec,
          trackFormat,
          trackSelectionReason,
          trackSelectionData,
          startTimeUs,
          endTimeUs,
          firstSegmentNum,
          trackType,
          trackFormat);
    } else {
      int segmentCount = 1;
      for (int i = 1; i < maxSegmentCount; i++) {
        RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i);
        @Nullable
        RangedUri mergedSegmentUri =
            segmentUri.attemptMerge(nextSegmentUri, representationHolder.selectedBaseUrl.url);
        if (mergedSegmentUri == null) {
          // Unable to merge segment fetches because the URIs do not merge.
          break;
        }
        segmentUri = mergedSegmentUri;
        segmentCount++;
      }
      long segmentNum = firstSegmentNum + segmentCount - 1;
      long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum);
      long periodDurationUs = representationHolder.periodDurationUs;
      long clippedEndTimeUs =
          periodDurationUs != C.TIME_UNSET && periodDurationUs <= endTimeUs
              ? periodDurationUs
              : C.TIME_UNSET;
      int flags =
          representationHolder.isSegmentAvailableAtFullNetworkSpeed(segmentNum, nowPeriodTimeUs)
              ? 0
              : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
      DataSpec dataSpec =
          DashUtil.buildDataSpec(
              representation,
              representationHolder.selectedBaseUrl.url,
              segmentUri,
              flags,
              /* httpRequestHeaders= */ ImmutableMap.of());
      if (cmcdDataFactory != null) {
        cmcdDataFactory
            .setChunkDurationUs(endTimeUs - startTimeUs)
            .setObjectType(CmcdData.Factory.getObjectType(trackSelection));
        @Nullable
        Pair<String, String> nextObjectAndRangeRequest =
            getNextObjectAndRangeRequest(firstSegmentNum, segmentUri, representationHolder);
        if (nextObjectAndRangeRequest != null) {
          cmcdDataFactory
              .setNextObjectRequest(nextObjectAndRangeRequest.first)
              .setNextRangeRequest(nextObjectAndRangeRequest.second);
        }
        CmcdData cmcdData = cmcdDataFactory.createCmcdData();
        dataSpec = cmcdData.addToDataSpec(dataSpec);
      }
      long sampleOffsetUs = -representation.presentationTimeOffsetUs;
      if (MimeTypes.isImage(trackFormat.sampleMimeType)) {
        sampleOffsetUs += startTimeUs;
      }
      return new ContainerMediaChunk(
          dataSource,
          dataSpec,
          trackFormat,
          trackSelectionReason,
          trackSelectionData,
          startTimeUs,
          endTimeUs,
          seekTimeUs,
          clippedEndTimeUs,
          firstSegmentNum,
          segmentCount,
          sampleOffsetUs,
          representationHolder.chunkExtractor);
    }
  }

  @Nullable
  private Pair<String, String> getNextObjectAndRangeRequest(
      long segmentNum, RangedUri segmentUri, RepresentationHolder representationHolder) {
    if (segmentNum + 1 >= representationHolder.getSegmentCount()) {
      return null;
    }
    RangedUri nextSegmentUri = representationHolder.getSegmentUrl(segmentNum + 1);
    Uri uri = segmentUri.resolveUri(representationHolder.selectedBaseUrl.url);
    Uri nextUri = nextSegmentUri.resolveUri(representationHolder.selectedBaseUrl.url);
    String nextObjectRequest = UriUtil.getRelativePath(uri, nextUri);

    String nextRangeRequest = nextSegmentUri.start + "-";
    if (nextSegmentUri.length != C.LENGTH_UNSET) {
      nextRangeRequest += (nextSegmentUri.start + nextSegmentUri.length);
    }
    return new Pair<>(nextObjectRequest, nextRangeRequest);
  }

  private RepresentationHolder updateSelectedBaseUrl(int trackIndex) {
    RepresentationHolder representationHolder = representationHolders[trackIndex];
    @Nullable
    BaseUrl selectedBaseUrl =
        baseUrlExclusionList.selectBaseUrl(representationHolder.representation.baseUrls);
    if (selectedBaseUrl != null && !selectedBaseUrl.equals(representationHolder.selectedBaseUrl)) {
      representationHolder = representationHolder.copyWithNewSelectedBaseUrl(selectedBaseUrl);
      representationHolders[trackIndex] = representationHolder;
    }
    return representationHolder;
  }

  // Protected classes.

  /** {@link MediaChunkIterator} wrapping a {@link RepresentationHolder}. */
  protected static final class RepresentationSegmentIterator extends BaseMediaChunkIterator {

    private final RepresentationHolder representationHolder;
    private final long nowPeriodTimeUs;

    /**
     * Creates iterator.
     *
     * @param representation The {@link RepresentationHolder} to wrap.
     * @param firstAvailableSegmentNum The number of the first available segment.
     * @param lastAvailableSegmentNum The number of the last available segment.
     * @param nowPeriodTimeUs The current time in microseconds since the start of the period used
     *     for calculating if segments are available at full network speed.
     */
    public RepresentationSegmentIterator(
        RepresentationHolder representation,
        long firstAvailableSegmentNum,
        long lastAvailableSegmentNum,
        long nowPeriodTimeUs) {
      super(/* fromIndex= */ firstAvailableSegmentNum, /* toIndex= */ lastAvailableSegmentNum);
      this.representationHolder = representation;
      this.nowPeriodTimeUs = nowPeriodTimeUs;
    }

    @Override
    public DataSpec getDataSpec() {
      checkInBounds();
      long currentIndex = getCurrentIndex();
      RangedUri segmentUri = representationHolder.getSegmentUrl(currentIndex);
      int flags =
          representationHolder.isSegmentAvailableAtFullNetworkSpeed(currentIndex, nowPeriodTimeUs)
              ? 0
              : DataSpec.FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED;
      return DashUtil.buildDataSpec(
          representationHolder.representation,
          representationHolder.selectedBaseUrl.url,
          segmentUri,
          flags,
          /* httpRequestHeaders= */ ImmutableMap.of());
    }

    @Override
    public long getChunkStartTimeUs() {
      checkInBounds();
      return representationHolder.getSegmentStartTimeUs(getCurrentIndex());
    }

    @Override
    public long getChunkEndTimeUs() {
      checkInBounds();
      return representationHolder.getSegmentEndTimeUs(getCurrentIndex());
    }
  }

  /** Holds information about a snapshot of a single {@link Representation}. */
  protected static final class RepresentationHolder {

    @Nullable /* package */ final ChunkExtractor chunkExtractor;

    public final Representation representation;
    public final BaseUrl selectedBaseUrl;
    @Nullable public final DashSegmentIndex segmentIndex;

    private final long periodDurationUs;
    private final long segmentNumShift;

    /* package */ RepresentationHolder(
        long periodDurationUs,
        Representation representation,
        BaseUrl selectedBaseUrl,
        @Nullable ChunkExtractor chunkExtractor,
        long segmentNumShift,
        @Nullable DashSegmentIndex segmentIndex) {
      this.periodDurationUs = periodDurationUs;
      this.representation = representation;
      this.selectedBaseUrl = selectedBaseUrl;
      this.segmentNumShift = segmentNumShift;
      this.chunkExtractor = chunkExtractor;
      this.segmentIndex = segmentIndex;
    }

    @CheckResult
    /* package */ RepresentationHolder copyWithNewRepresentation(
        long newPeriodDurationUs, Representation newRepresentation)
        throws BehindLiveWindowException {
      @Nullable DashSegmentIndex oldIndex = representation.getIndex();
      @Nullable DashSegmentIndex newIndex = newRepresentation.getIndex();

      if (oldIndex == null) {
        // Segment numbers cannot shift if the index isn't defined by the manifest.
        return new RepresentationHolder(
            newPeriodDurationUs,
            newRepresentation,
            selectedBaseUrl,
            chunkExtractor,
            segmentNumShift,
            oldIndex);
      }

      if (!oldIndex.isExplicit()) {
        // Segment numbers cannot shift if the index isn't explicit.
        return new RepresentationHolder(
            newPeriodDurationUs,
            newRepresentation,
            selectedBaseUrl,
            chunkExtractor,
            segmentNumShift,
            newIndex);
      }

      long oldIndexSegmentCount = oldIndex.getSegmentCount(newPeriodDurationUs);
      if (oldIndexSegmentCount == 0) {
        // Segment numbers cannot shift if the old index was empty.
        return new RepresentationHolder(
            newPeriodDurationUs,
            newRepresentation,
            selectedBaseUrl,
            chunkExtractor,
            segmentNumShift,
            newIndex);
      }

      checkStateNotNull(newIndex);

      long oldIndexFirstSegmentNum = oldIndex.getFirstSegmentNum();
      long oldIndexStartTimeUs = oldIndex.getTimeUs(oldIndexFirstSegmentNum);
      long oldIndexLastSegmentNum = oldIndexFirstSegmentNum + oldIndexSegmentCount - 1;
      long oldIndexEndTimeUs =
          oldIndex.getTimeUs(oldIndexLastSegmentNum)
              + oldIndex.getDurationUs(oldIndexLastSegmentNum, newPeriodDurationUs);
      long newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
      long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
      long newSegmentNumShift = segmentNumShift;
      if (oldIndexEndTimeUs == newIndexStartTimeUs) {
        // The new index continues where the old one ended, with no overlap.
        newSegmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum;
      } else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
        // There's a gap between the old index and the new one which means we've slipped behind the
        // live window and can't proceed.
        throw new BehindLiveWindowException();
      } else if (newIndexStartTimeUs < oldIndexStartTimeUs) {
        // The new index overlaps with (but does not have a start position contained within) the old
        // index. This can only happen if extra segments have been added to the start of the index.
        newSegmentNumShift -=
            newIndex.getSegmentNum(oldIndexStartTimeUs, newPeriodDurationUs)
                - oldIndexFirstSegmentNum;
      } else {
        // The new index overlaps with (and has a start position contained within) the old index.
        newSegmentNumShift +=
            oldIndex.getSegmentNum(newIndexStartTimeUs, newPeriodDurationUs)
                - newIndexFirstSegmentNum;
      }
      return new RepresentationHolder(
          newPeriodDurationUs,
          newRepresentation,
          selectedBaseUrl,
          chunkExtractor,
          newSegmentNumShift,
          newIndex);
    }

    @CheckResult
    /* package */ RepresentationHolder copyWithNewSegmentIndex(DashSegmentIndex segmentIndex) {
      return new RepresentationHolder(
          periodDurationUs,
          representation,
          selectedBaseUrl,
          chunkExtractor,
          segmentNumShift,
          segmentIndex);
    }

    @CheckResult
    /* package */ RepresentationHolder copyWithNewSelectedBaseUrl(BaseUrl selectedBaseUrl) {
      return new RepresentationHolder(
          periodDurationUs,
          representation,
          selectedBaseUrl,
          chunkExtractor,
          segmentNumShift,
          segmentIndex);
    }

    public long getFirstSegmentNum() {
      return checkStateNotNull(segmentIndex).getFirstSegmentNum() + segmentNumShift;
    }

    public long getFirstAvailableSegmentNum(long nowUnixTimeUs) {
      return checkStateNotNull(segmentIndex)
              .getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs)
          + segmentNumShift;
    }

    public long getSegmentCount() {
      return checkStateNotNull(segmentIndex).getSegmentCount(periodDurationUs);
    }

    public long getSegmentStartTimeUs(long segmentNum) {
      return checkStateNotNull(segmentIndex).getTimeUs(segmentNum - segmentNumShift);
    }

    public long getSegmentEndTimeUs(long segmentNum) {
      return getSegmentStartTimeUs(segmentNum)
          + checkStateNotNull(segmentIndex)
              .getDurationUs(segmentNum - segmentNumShift, periodDurationUs);
    }

    public long getSegmentNum(long positionUs) {
      return checkStateNotNull(segmentIndex).getSegmentNum(positionUs, periodDurationUs)
          + segmentNumShift;
    }

    public RangedUri getSegmentUrl(long segmentNum) {
      return checkStateNotNull(segmentIndex).getSegmentUrl(segmentNum - segmentNumShift);
    }

    public long getLastAvailableSegmentNum(long nowUnixTimeUs) {
      return getFirstAvailableSegmentNum(nowUnixTimeUs)
          + checkStateNotNull(segmentIndex)
              .getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs)
          - 1;
    }

    public boolean isSegmentAvailableAtFullNetworkSpeed(long segmentNum, long nowPeriodTimeUs) {
      if (checkStateNotNull(segmentIndex).isExplicit()) {
        // We don't support segment availability for explicit indices (internal ref: b/172894901).
        // Hence, also assume all segments in explicit indices are always available at full network
        // speed even if they end in the future.
        return true;
      }
      return nowPeriodTimeUs == C.TIME_UNSET || getSegmentEndTimeUs(segmentNum) <= nowPeriodTimeUs;
    }
  }
}