public final class

ConcatenatingMediaSource

extends CompositeMediaSource<androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder>

 java.lang.Object

androidx.media3.exoplayer.source.BaseMediaSource

androidx.media3.exoplayer.source.CompositeMediaSource<androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder>

↳androidx.media3.exoplayer.source.ConcatenatingMediaSource

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

Concatenates multiple MediaSources. The list of MediaSources can be modified during playback. It is valid for the same MediaSource instance to be present more than once in the concatenation. Access to this class is thread-safe.

Summary

Constructors
publicConcatenatingMediaSource(boolean isAtomic, boolean useLazyPreparation, ShuffleOrder shuffleOrder, MediaSource mediaSources[])

publicConcatenatingMediaSource(boolean isAtomic, MediaSource mediaSources[])

publicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource mediaSources[])

publicConcatenatingMediaSource(MediaSource mediaSources[])

Methods
public synchronized voidaddMediaSource(int index, MediaSource mediaSource)

Adds a MediaSource to the playlist.

public synchronized voidaddMediaSource(int index, MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)

Adds a MediaSource to the playlist and executes a custom action on completion.

public synchronized voidaddMediaSource(MediaSource mediaSource)

Appends a MediaSource to the playlist.

public synchronized voidaddMediaSource(MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)

Appends a MediaSource to the playlist and executes a custom action on completion.

public synchronized voidaddMediaSources(java.util.Collection<MediaSource> mediaSources)

Appends multiple MediaSources to the playlist.

public synchronized voidaddMediaSources(java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)

Appends multiple MediaSources to the playlist and executes a custom action on completion.

public synchronized voidaddMediaSources(int index, java.util.Collection<MediaSource> mediaSources)

Adds multiple MediaSources to the playlist.

public synchronized voidaddMediaSources(int index, java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)

Adds multiple MediaSources to the playlist and executes a custom action on completion.

public synchronized voidclear()

Clears the playlist.

public synchronized voidclear(Handler handler, java.lang.Runnable onCompletionAction)

Clears the playlist and executes a custom action on completion.

public MediaPeriodcreatePeriod(MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)

protected voiddisableInternal()

Disables the source, see MediaSource.disable(MediaSource.MediaSourceCaller).

protected voidenableInternal()

Enables the source, see MediaSource.enable(MediaSource.MediaSourceCaller).

public synchronized TimelinegetInitialTimeline()

public MediaItemgetMediaItem()

protected MediaSource.MediaPeriodIdgetMediaPeriodIdForChildMediaPeriodId(java.lang.Object id, MediaSource.MediaPeriodId mediaPeriodId)

Returns the in the composite source corresponding to the specified in a child source.

public synchronized MediaSourcegetMediaSource(int index)

Returns the MediaSource at a specified index.

public synchronized intgetSize()

Returns the number of media sources in the playlist.

protected intgetWindowIndexForChildWindowIndex(java.lang.Object id, int windowIndex)

Returns the window index in the composite source corresponding to the specified window index in a child source.

public booleanisSingleWindow()

public synchronized voidmoveMediaSource(int currentIndex, int newIndex)

Moves an existing MediaSource within the playlist.

public synchronized voidmoveMediaSource(int currentIndex, int newIndex, Handler handler, java.lang.Runnable onCompletionAction)

Moves an existing MediaSource within the playlist and executes a custom action on completion.

protected abstract voidonChildSourceInfoRefreshed(java.lang.Object id, MediaSource mediaSource, Timeline timeline)

Called when the source info of a child source has been refreshed.

protected abstract voidprepareSourceInternal(TransferListener mediaTransferListener)

Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId).

public voidreleasePeriod(MediaPeriod mediaPeriod)

protected abstract voidreleaseSourceInternal()

Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller).

public synchronized MediaSourceremoveMediaSource(int index)

Removes a MediaSource from the playlist.

public synchronized MediaSourceremoveMediaSource(int index, Handler handler, java.lang.Runnable onCompletionAction)

Removes a MediaSource from the playlist and executes a custom action on completion.

public synchronized voidremoveMediaSourceRange(int fromIndex, int toIndex)

Removes a range of MediaSources from the playlist, by specifying an initial index (included) and a final index (excluded).

public synchronized voidremoveMediaSourceRange(int fromIndex, int toIndex, Handler handler, java.lang.Runnable onCompletionAction)

Removes a range of MediaSources from the playlist, by specifying an initial index (included) and a final index (excluded), and executes a custom action on completion.

public synchronized voidsetShuffleOrder(ShuffleOrder shuffleOrder)

Sets a new shuffle order to use when shuffling the child media sources.

public synchronized voidsetShuffleOrder(ShuffleOrder shuffleOrder, Handler handler, java.lang.Runnable onCompletionAction)

Sets a new shuffle order to use when shuffling the child media sources.

from CompositeMediaSource<T>disableChildSource, enableChildSource, getMediaTimeForChildMediaTime, maybeThrowSourceInfoRefreshError, prepareChildSource, releaseChildSource
from BaseMediaSourceaddDrmEventListener, addEventListener, createDrmEventDispatcher, createDrmEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, disable, enable, getPlayerId, isEnabled, prepareSource, refreshSourceInfo, releaseSource, removeDrmEventListener, removeEventListener
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public ConcatenatingMediaSource(MediaSource mediaSources[])

Parameters:

mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.

public ConcatenatingMediaSource(boolean isAtomic, MediaSource mediaSources[])

Parameters:

isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated as a single item for repeating and shuffling.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.

public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource mediaSources[])

Parameters:

isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated as a single item for repeating and shuffling.
shuffleOrder: The ShuffleOrder to use when shuffling the child media sources.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.

public ConcatenatingMediaSource(boolean isAtomic, boolean useLazyPreparation, ShuffleOrder shuffleOrder, MediaSource mediaSources[])

Parameters:

isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated as a single item for repeating and shuffling.
useLazyPreparation: Whether playlist items are prepared lazily. If false, all manifest loads and other initial preparation steps happen immediately. If true, these initial preparations are triggered only when the player starts buffering the media.
shuffleOrder: The ShuffleOrder to use when shuffling the child media sources.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.

Methods

public synchronized Timeline getInitialTimeline()

public boolean isSingleWindow()

public synchronized void addMediaSource(MediaSource mediaSource)

Appends a MediaSource to the playlist.

Parameters:

mediaSource: The MediaSource to be added to the list.

public synchronized void addMediaSource(MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)

Appends a MediaSource to the playlist and executes a custom action on completion.

Parameters:

mediaSource: The MediaSource to be added to the list.
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media source has been added to the playlist.

public synchronized void addMediaSource(int index, MediaSource mediaSource)

Adds a MediaSource to the playlist.

Parameters:

index: The index at which the new MediaSource will be inserted. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSource: The MediaSource to be added to the list.

public synchronized void addMediaSource(int index, MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)

Adds a MediaSource to the playlist and executes a custom action on completion.

Parameters:

index: The index at which the new MediaSource will be inserted. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSource: The MediaSource to be added to the list.
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media source has been added to the playlist.

public synchronized void addMediaSources(java.util.Collection<MediaSource> mediaSources)

Appends multiple MediaSources to the playlist.

Parameters:

mediaSources: A collection of MediaSources to be added to the list. The media sources are added in the order in which they appear in this collection.

public synchronized void addMediaSources(java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)

Appends multiple MediaSources to the playlist and executes a custom action on completion.

Parameters:

mediaSources: A collection of MediaSources to be added to the list. The media sources are added in the order in which they appear in this collection.
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media sources have been added to the playlist.

public synchronized void addMediaSources(int index, java.util.Collection<MediaSource> mediaSources)

Adds multiple MediaSources to the playlist.

Parameters:

index: The index at which the new MediaSources will be inserted. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSources: A collection of MediaSources to be added to the list. The media sources are added in the order in which they appear in this collection.

public synchronized void addMediaSources(int index, java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)

Adds multiple MediaSources to the playlist and executes a custom action on completion.

Parameters:

index: The index at which the new MediaSources will be inserted. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSources: A collection of MediaSources to be added to the list. The media sources are added in the order in which they appear in this collection.
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media sources have been added to the playlist.

public synchronized MediaSource removeMediaSource(int index)

Removes a MediaSource from the playlist.

Note: If you want to move the instance, it's preferable to use ConcatenatingMediaSource.moveMediaSource(int, int) instead.

Note: If you want to remove a set of contiguous sources, it's preferable to use ConcatenatingMediaSource.removeMediaSourceRange(int, int) instead.

Parameters:

index: The index at which the media source will be removed. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().

Returns:

The removed MediaSource.

public synchronized MediaSource removeMediaSource(int index, Handler handler, java.lang.Runnable onCompletionAction)

Removes a MediaSource from the playlist and executes a custom action on completion.

Note: If you want to move the instance, it's preferable to use ConcatenatingMediaSource.moveMediaSource(int, int, Handler, Runnable) instead.

Note: If you want to remove a set of contiguous sources, it's preferable to use ConcatenatingMediaSource.removeMediaSourceRange(int, int, Handler, Runnable) instead.

Parameters:

index: The index at which the media source will be removed. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media source has been removed from the playlist.

Returns:

The removed MediaSource.

public synchronized void removeMediaSourceRange(int fromIndex, int toIndex)

Removes a range of MediaSources from the playlist, by specifying an initial index (included) and a final index (excluded).

Note: when specified range is empty, no actual media source is removed and no exception is thrown.

Parameters:

fromIndex: The initial range index, pointing to the first media source that will be removed. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
toIndex: The final range index, pointing to the first media source that will be left untouched. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().

public synchronized void removeMediaSourceRange(int fromIndex, int toIndex, Handler handler, java.lang.Runnable onCompletionAction)

Removes a range of MediaSources from the playlist, by specifying an initial index (included) and a final index (excluded), and executes a custom action on completion.

Note: when specified range is empty, no actual media source is removed and no exception is thrown.

Parameters:

fromIndex: The initial range index, pointing to the first media source that will be removed. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
toIndex: The final range index, pointing to the first media source that will be left untouched. This index must be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media source range has been removed from the playlist.

public synchronized void moveMediaSource(int currentIndex, int newIndex)

Moves an existing MediaSource within the playlist.

Parameters:

currentIndex: The current index of the media source in the playlist. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().
newIndex: The target index of the media source in the playlist. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().

public synchronized void moveMediaSource(int currentIndex, int newIndex, Handler handler, java.lang.Runnable onCompletionAction)

Moves an existing MediaSource within the playlist and executes a custom action on completion.

Parameters:

currentIndex: The current index of the media source in the playlist. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().
newIndex: The target index of the media source in the playlist. This index must be in the range of 0 <= index < ConcatenatingMediaSource.getSize().
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the media source has been moved.

public synchronized void clear()

Clears the playlist.

public synchronized void clear(Handler handler, java.lang.Runnable onCompletionAction)

Clears the playlist and executes a custom action on completion.

Parameters:

handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the playlist has been cleared.

public synchronized int getSize()

Returns the number of media sources in the playlist.

public synchronized MediaSource getMediaSource(int index)

Returns the MediaSource at a specified index.

Parameters:

index: An index in the range of 0 <= index <= ConcatenatingMediaSource.getSize().

Returns:

The MediaSource at this index.

public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder)

Sets a new shuffle order to use when shuffling the child media sources.

Parameters:

shuffleOrder: A ShuffleOrder.

public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder, Handler handler, java.lang.Runnable onCompletionAction)

Sets a new shuffle order to use when shuffling the child media sources.

Parameters:

shuffleOrder: A ShuffleOrder.
handler: The Handler to run onCompletionAction.
onCompletionAction: A java.lang.Runnable which is executed immediately after the shuffle order has been changed.

public MediaItem getMediaItem()

protected abstract void prepareSourceInternal(TransferListener mediaTransferListener)

Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId). This method is called at most once until the next call to BaseMediaSource.releaseSourceInternal().

Parameters:

mediaTransferListener: The transfer listener which should be informed of any media data transfers. May be null if no listener is available. Note that this listener should usually be only informed of transfers related to the media loads and not of auxiliary loads for manifests and other data.

protected void enableInternal()

Enables the source, see MediaSource.enable(MediaSource.MediaSourceCaller).

public MediaPeriod createPeriod(MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)

public void releasePeriod(MediaPeriod mediaPeriod)

protected void disableInternal()

Disables the source, see MediaSource.disable(MediaSource.MediaSourceCaller).

protected abstract void releaseSourceInternal()

Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller). This method is called exactly once after each call to BaseMediaSource.prepareSourceInternal(TransferListener).

protected abstract void onChildSourceInfoRefreshed(java.lang.Object id, MediaSource mediaSource, Timeline timeline)

Called when the source info of a child source has been refreshed.

Parameters:

id: The unique id used to prepare the child source.
mediaSource: The child source whose source info has been refreshed.
timeline: The timeline of the child source.

protected MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(java.lang.Object id, MediaSource.MediaPeriodId mediaPeriodId)

Returns the in the composite source corresponding to the specified in a child source. The default implementation does not change the media period id.

Parameters:

id: The unique id used to prepare the child source.
mediaPeriodId: A of the child source.

Returns:

The corresponding in the composite source. Null if no corresponding media period id can be determined.

protected int getWindowIndexForChildWindowIndex(java.lang.Object id, int windowIndex)

Returns the window index in the composite source corresponding to the specified window index in a child source. The default implementation does not change the window index.

Parameters:

id: The unique id used to prepare the child source.
windowIndex: A window index of the child source.

Returns:

The corresponding window index in the composite source.

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.source;

import static java.lang.Math.max;
import static java.lang.Math.min;

import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.AbstractConcatenatedTimeline;
import androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.exoplayer.upstream.Allocator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
 * during playback. It is valid for the same {@link MediaSource} instance to be present more than
 * once in the concatenation. Access to this class is thread-safe.
 */
@UnstableApi
public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {

  private static final int MSG_ADD = 0;
  private static final int MSG_REMOVE = 1;
  private static final int MSG_MOVE = 2;
  private static final int MSG_SET_SHUFFLE_ORDER = 3;
  private static final int MSG_UPDATE_TIMELINE = 4;
  private static final int MSG_ON_COMPLETION = 5;

  private static final MediaItem EMPTY_MEDIA_ITEM =
      new MediaItem.Builder().setUri(Uri.EMPTY).build();

  // Accessed on any thread.
  @GuardedBy("this")
  private final List<MediaSourceHolder> mediaSourcesPublic;

  @GuardedBy("this")
  private final Set<HandlerAndRunnable> pendingOnCompletionActions;

  @GuardedBy("this")
  @Nullable
  private Handler playbackThreadHandler;

  // Accessed on the playback thread only.
  private final List<MediaSourceHolder> mediaSourceHolders;
  private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
  private final Map<Object, MediaSourceHolder> mediaSourceByUid;
  private final Set<MediaSourceHolder> enabledMediaSourceHolders;
  private final boolean isAtomic;
  private final boolean useLazyPreparation;

  private boolean timelineUpdateScheduled;
  private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
  private ShuffleOrder shuffleOrder;

  /**
   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
   *     MediaSource} instance to be present more than once in the array.
   */
  public ConcatenatingMediaSource(MediaSource... mediaSources) {
    this(/* isAtomic= */ false, mediaSources);
  }

  /**
   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
   *     as a single item for repeating and shuffling.
   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
   *     MediaSource} instance to be present more than once in the array.
   */
  public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) {
    this(isAtomic, new DefaultShuffleOrder(0), mediaSources);
  }

  /**
   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
   *     as a single item for repeating and shuffling.
   * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
   *     MediaSource} instance to be present more than once in the array.
   */
  public ConcatenatingMediaSource(
      boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) {
    this(isAtomic, /* useLazyPreparation= */ false, shuffleOrder, mediaSources);
  }

  /**
   * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
   *     as a single item for repeating and shuffling.
   * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
   *     loads and other initial preparation steps happen immediately. If true, these initial
   *     preparations are triggered only when the player starts buffering the media.
   * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
   *     MediaSource} instance to be present more than once in the array.
   */
  @SuppressWarnings("initialization")
  public ConcatenatingMediaSource(
      boolean isAtomic,
      boolean useLazyPreparation,
      ShuffleOrder shuffleOrder,
      MediaSource... mediaSources) {
    for (MediaSource mediaSource : mediaSources) {
      Assertions.checkNotNull(mediaSource);
    }
    this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
    this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
    this.mediaSourceByUid = new HashMap<>();
    this.mediaSourcesPublic = new ArrayList<>();
    this.mediaSourceHolders = new ArrayList<>();
    this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
    this.pendingOnCompletionActions = new HashSet<>();
    this.enabledMediaSourceHolders = new HashSet<>();
    this.isAtomic = isAtomic;
    this.useLazyPreparation = useLazyPreparation;
    addMediaSources(Arrays.asList(mediaSources));
  }

  @Override
  public synchronized Timeline getInitialTimeline() {
    ShuffleOrder shuffleOrder =
        this.shuffleOrder.getLength() != mediaSourcesPublic.size()
            ? this.shuffleOrder
                .cloneAndClear()
                .cloneAndInsert(
                    /* insertionIndex= */ 0, /* insertionCount= */ mediaSourcesPublic.size())
            : this.shuffleOrder;
    return new ConcatenatedTimeline(mediaSourcesPublic, shuffleOrder, isAtomic);
  }

  @Override
  public boolean isSingleWindow() {
    return false;
  }

  /**
   * Appends a {@link MediaSource} to the playlist.
   *
   * @param mediaSource The {@link MediaSource} to be added to the list.
   */
  public synchronized void addMediaSource(MediaSource mediaSource) {
    addMediaSource(mediaSourcesPublic.size(), mediaSource);
  }

  /**
   * Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
   *
   * @param mediaSource The {@link MediaSource} to be added to the list.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     source has been added to the playlist.
   */
  public synchronized void addMediaSource(
      MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
    addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
  }

  /**
   * Adds a {@link MediaSource} to the playlist.
   *
   * @param index The index at which the new {@link MediaSource} will be inserted. This index must
   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param mediaSource The {@link MediaSource} to be added to the list.
   */
  public synchronized void addMediaSource(int index, MediaSource mediaSource) {
    addPublicMediaSources(
        index,
        Collections.singletonList(mediaSource),
        /* handler= */ null,
        /* onCompletionAction= */ null);
  }

  /**
   * Adds a {@link MediaSource} to the playlist and executes a custom action on completion.
   *
   * @param index The index at which the new {@link MediaSource} will be inserted. This index must
   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param mediaSource The {@link MediaSource} to be added to the list.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     source has been added to the playlist.
   */
  public synchronized void addMediaSource(
      int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
    addPublicMediaSources(
        index, Collections.singletonList(mediaSource), handler, onCompletionAction);
  }

  /**
   * Appends multiple {@link MediaSource}s to the playlist.
   *
   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
   *     sources are added in the order in which they appear in this collection.
   */
  public synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
    addPublicMediaSources(
        mediaSourcesPublic.size(),
        mediaSources,
        /* handler= */ null,
        /* onCompletionAction= */ null);
  }

  /**
   * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on
   * completion.
   *
   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
   *     sources are added in the order in which they appear in this collection.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     sources have been added to the playlist.
   */
  public synchronized void addMediaSources(
      Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
    addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
  }

  /**
   * Adds multiple {@link MediaSource}s to the playlist.
   *
   * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
   *     sources are added in the order in which they appear in this collection.
   */
  public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
    addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
  }

  /**
   * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.
   *
   * @param index The index at which the new {@link MediaSource}s will be inserted. This index must
   *     be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
   *     sources are added in the order in which they appear in this collection.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     sources have been added to the playlist.
   */
  public synchronized void addMediaSources(
      int index,
      Collection<MediaSource> mediaSources,
      Handler handler,
      Runnable onCompletionAction) {
    addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
  }

  /**
   * Removes a {@link MediaSource} from the playlist.
   *
   * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
   * int)} instead.
   *
   * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link
   * #removeMediaSourceRange(int, int)} instead.
   *
   * @param index The index at which the media source will be removed. This index must be in the
   *     range of 0 &lt;= index &lt; {@link #getSize()}.
   * @return The removed {@link MediaSource}.
   */
  public synchronized MediaSource removeMediaSource(int index) {
    MediaSource removedMediaSource = getMediaSource(index);
    removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
    return removedMediaSource;
  }

  /**
   * Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
   *
   * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
   * int, Handler, Runnable)} instead.
   *
   * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link
   * #removeMediaSourceRange(int, int, Handler, Runnable)} instead.
   *
   * @param index The index at which the media source will be removed. This index must be in the
   *     range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     source has been removed from the playlist.
   * @return The removed {@link MediaSource}.
   */
  public synchronized MediaSource removeMediaSource(
      int index, Handler handler, Runnable onCompletionAction) {
    MediaSource removedMediaSource = getMediaSource(index);
    removePublicMediaSources(index, index + 1, handler, onCompletionAction);
    return removedMediaSource;
  }

  /**
   * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index
   * (included) and a final index (excluded).
   *
   * <p>Note: when specified range is empty, no actual media source is removed and no exception is
   * thrown.
   *
   * @param fromIndex The initial range index, pointing to the first media source that will be
   *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param toIndex The final range index, pointing to the first media source that will be left
   *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
   *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
   */
  public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
    removePublicMediaSources(
        fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
  }

  /**
   * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index
   * (included) and a final index (excluded), and executes a custom action on completion.
   *
   * <p>Note: when specified range is empty, no actual media source is removed and no exception is
   * thrown.
   *
   * @param fromIndex The initial range index, pointing to the first media source that will be
   *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param toIndex The final range index, pointing to the first media source that will be left
   *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     source range has been removed from the playlist.
   * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
   *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
   */
  public synchronized void removeMediaSourceRange(
      int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
    removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
  }

  /**
   * Moves an existing {@link MediaSource} within the playlist.
   *
   * @param currentIndex The current index of the media source in the playlist. This index must be
   *     in the range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param newIndex The target index of the media source in the playlist. This index must be in the
   *     range of 0 &lt;= index &lt; {@link #getSize()}.
   */
  public synchronized void moveMediaSource(int currentIndex, int newIndex) {
    movePublicMediaSource(
        currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
  }

  /**
   * Moves an existing {@link MediaSource} within the playlist and executes a custom action on
   * completion.
   *
   * @param currentIndex The current index of the media source in the playlist. This index must be
   *     in the range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param newIndex The target index of the media source in the playlist. This index must be in the
   *     range of 0 &lt;= index &lt; {@link #getSize()}.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
   *     source has been moved.
   */
  public synchronized void moveMediaSource(
      int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
    movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
  }

  /** Clears the playlist. */
  public synchronized void clear() {
    removeMediaSourceRange(0, getSize());
  }

  /**
   * Clears the playlist and executes a custom action on completion.
   *
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
   *     has been cleared.
   */
  public synchronized void clear(Handler handler, Runnable onCompletionAction) {
    removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
  }

  /** Returns the number of media sources in the playlist. */
  public synchronized int getSize() {
    return mediaSourcesPublic.size();
  }

  /**
   * Returns the {@link MediaSource} at a specified index.
   *
   * @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.
   * @return The {@link MediaSource} at this index.
   */
  public synchronized MediaSource getMediaSource(int index) {
    return mediaSourcesPublic.get(index).mediaSource;
  }

  /**
   * Sets a new shuffle order to use when shuffling the child media sources.
   *
   * @param shuffleOrder A {@link ShuffleOrder}.
   */
  public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
    setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
  }

  /**
   * Sets a new shuffle order to use when shuffling the child media sources.
   *
   * @param shuffleOrder A {@link ShuffleOrder}.
   * @param handler The {@link Handler} to run {@code onCompletionAction}.
   * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
   *     order has been changed.
   */
  public synchronized void setShuffleOrder(
      ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
    setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
  }

  // CompositeMediaSource implementation.

  @Override
  public MediaItem getMediaItem() {
    // This method is actually never called because getInitialTimeline is implemented and hence the
    // MaskingMediaSource does not need to create a placeholder timeline for this media source.
    return EMPTY_MEDIA_ITEM;
  }

  @Override
  protected synchronized void prepareSourceInternal(
      @Nullable TransferListener mediaTransferListener) {
    super.prepareSourceInternal(mediaTransferListener);
    playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
    if (mediaSourcesPublic.isEmpty()) {
      updateTimelineAndScheduleOnCompletionActions();
    } else {
      shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
      addMediaSourcesInternal(0, mediaSourcesPublic);
      scheduleTimelineUpdate();
    }
  }

  @SuppressWarnings("MissingSuperCall")
  @Override
  protected void enableInternal() {
    // Suppress enabling all child sources here as they can be lazily enabled when creating periods.
  }

  @Override
  public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
    Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
    MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
    @Nullable MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
    if (holder == null) {
      // Stale event. The media source has already been removed.
      holder = new MediaSourceHolder(new FakeMediaSource(), useLazyPreparation);
      holder.isRemoved = true;
      prepareChildSource(holder, holder.mediaSource);
    }
    enableMediaSource(holder);
    holder.activeMediaPeriodIds.add(childMediaPeriodId);
    MediaPeriod mediaPeriod =
        holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
    mediaSourceByMediaPeriod.put(mediaPeriod, holder);
    disableUnusedMediaSources();
    return mediaPeriod;
  }

  @Override
  public void releasePeriod(MediaPeriod mediaPeriod) {
    MediaSourceHolder holder =
        Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
    holder.mediaSource.releasePeriod(mediaPeriod);
    holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
    if (!mediaSourceByMediaPeriod.isEmpty()) {
      disableUnusedMediaSources();
    }
    maybeReleaseChildSource(holder);
  }

  @Override
  protected void disableInternal() {
    super.disableInternal();
    enabledMediaSourceHolders.clear();
  }

  @Override
  protected synchronized void releaseSourceInternal() {
    super.releaseSourceInternal();
    mediaSourceHolders.clear();
    enabledMediaSourceHolders.clear();
    mediaSourceByUid.clear();
    shuffleOrder = shuffleOrder.cloneAndClear();
    if (playbackThreadHandler != null) {
      playbackThreadHandler.removeCallbacksAndMessages(null);
      playbackThreadHandler = null;
    }
    timelineUpdateScheduled = false;
    nextTimelineUpdateOnCompletionActions.clear();
    dispatchOnCompletionActions(pendingOnCompletionActions);
  }

  @Override
  protected void onChildSourceInfoRefreshed(
      MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) {
    updateMediaSourceInternal(mediaSourceHolder, timeline);
  }

  @Override
  @Nullable
  protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
      MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {
    for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
      // Ensure the reported media period id has the same window sequence number as the one created
      // by this media source. Otherwise it does not belong to this child source.
      if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
          == mediaPeriodId.windowSequenceNumber) {
        Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
        return mediaPeriodId.copyWithPeriodUid(periodUid);
      }
    }
    return null;
  }

  @Override
  protected int getWindowIndexForChildWindowIndex(
      MediaSourceHolder mediaSourceHolder, int windowIndex) {
    return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
  }

  // Internal methods. Called from any thread.

  @GuardedBy("this")
  private void addPublicMediaSources(
      int index,
      Collection<MediaSource> mediaSources,
      @Nullable Handler handler,
      @Nullable Runnable onCompletionAction) {
    Assertions.checkArgument((handler == null) == (onCompletionAction == null));
    @Nullable Handler playbackThreadHandler = this.playbackThreadHandler;
    for (MediaSource mediaSource : mediaSources) {
      Assertions.checkNotNull(mediaSource);
    }
    List<MediaSourceHolder> mediaSourceHolders = new ArrayList<>(mediaSources.size());
    for (MediaSource mediaSource : mediaSources) {
      mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation));
    }
    mediaSourcesPublic.addAll(index, mediaSourceHolders);
    if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
      @Nullable
      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
      playbackThreadHandler
          .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
          .sendToTarget();
    } else if (onCompletionAction != null && handler != null) {
      handler.post(onCompletionAction);
    }
  }

  @GuardedBy("this")
  private void removePublicMediaSources(
      int fromIndex,
      int toIndex,
      @Nullable Handler handler,
      @Nullable Runnable onCompletionAction) {
    Assertions.checkArgument((handler == null) == (onCompletionAction == null));
    @Nullable Handler playbackThreadHandler = this.playbackThreadHandler;
    Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
    if (playbackThreadHandler != null) {
      @Nullable
      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
      playbackThreadHandler
          .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
          .sendToTarget();
    } else if (onCompletionAction != null && handler != null) {
      handler.post(onCompletionAction);
    }
  }

  @GuardedBy("this")
  private void movePublicMediaSource(
      int currentIndex,
      int newIndex,
      @Nullable Handler handler,
      @Nullable Runnable onCompletionAction) {
    Assertions.checkArgument((handler == null) == (onCompletionAction == null));
    @Nullable Handler playbackThreadHandler = this.playbackThreadHandler;
    mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
    if (playbackThreadHandler != null) {
      @Nullable
      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
      playbackThreadHandler
          .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
          .sendToTarget();
    } else if (onCompletionAction != null && handler != null) {
      handler.post(onCompletionAction);
    }
  }

  @GuardedBy("this")
  private void setPublicShuffleOrder(
      ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
    Assertions.checkArgument((handler == null) == (onCompletionAction == null));
    @Nullable Handler playbackThreadHandler = this.playbackThreadHandler;
    if (playbackThreadHandler != null) {
      int size = getSize();
      if (shuffleOrder.getLength() != size) {
        shuffleOrder =
            shuffleOrder
                .cloneAndClear()
                .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
      }
      @Nullable
      HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
      playbackThreadHandler
          .obtainMessage(
              MSG_SET_SHUFFLE_ORDER,
              new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
          .sendToTarget();
    } else {
      this.shuffleOrder =
          shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
      if (onCompletionAction != null && handler != null) {
        handler.post(onCompletionAction);
      }
    }
  }

  @GuardedBy("this")
  @Nullable
  private HandlerAndRunnable createOnCompletionAction(
      @Nullable Handler handler, @Nullable Runnable runnable) {
    if (handler == null || runnable == null) {
      return null;
    }
    HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
    pendingOnCompletionActions.add(handlerAndRunnable);
    return handlerAndRunnable;
  }

  // Internal methods. Called on the playback thread.

  @SuppressWarnings("unchecked")
  private boolean handleMessage(Message msg) {
    switch (msg.what) {
      case MSG_ADD:
        MessageData<Collection<MediaSourceHolder>> addMessage =
            (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
        shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
        addMediaSourcesInternal(addMessage.index, addMessage.customData);
        scheduleTimelineUpdate(addMessage.onCompletionAction);
        break;
      case MSG_REMOVE:
        MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
        int fromIndex = removeMessage.index;
        int toIndex = removeMessage.customData;
        if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) {
          shuffleOrder = shuffleOrder.cloneAndClear();
        } else {
          shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex);
        }
        for (int index = toIndex - 1; index >= fromIndex; index--) {
          removeMediaSourceInternal(index);
        }
        scheduleTimelineUpdate(removeMessage.onCompletionAction);
        break;
      case MSG_MOVE:
        MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
        shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
        shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
        moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
        scheduleTimelineUpdate(moveMessage.onCompletionAction);
        break;
      case MSG_SET_SHUFFLE_ORDER:
        MessageData<ShuffleOrder> shuffleOrderMessage =
            (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
        shuffleOrder = shuffleOrderMessage.customData;
        scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
        break;
      case MSG_UPDATE_TIMELINE:
        updateTimelineAndScheduleOnCompletionActions();
        break;
      case MSG_ON_COMPLETION:
        Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
        dispatchOnCompletionActions(actions);
        break;
      default:
        throw new IllegalStateException();
    }
    return true;
  }

  private void scheduleTimelineUpdate() {
    scheduleTimelineUpdate(/* onCompletionAction= */ null);
  }

  private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
    if (!timelineUpdateScheduled) {
      getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
      timelineUpdateScheduled = true;
    }
    if (onCompletionAction != null) {
      nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
    }
  }

  private void updateTimelineAndScheduleOnCompletionActions() {
    timelineUpdateScheduled = false;
    Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
    nextTimelineUpdateOnCompletionActions = new HashSet<>();
    refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic));
    getPlaybackThreadHandlerOnPlaybackThread()
        .obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
        .sendToTarget();
  }

  @SuppressWarnings("GuardedBy")
  private Handler getPlaybackThreadHandlerOnPlaybackThread() {
    // Write access to this value happens on the playback thread only, so playback thread reads
    // don't need to be synchronized.
    return Assertions.checkNotNull(playbackThreadHandler);
  }

  private synchronized void dispatchOnCompletionActions(
      Set<HandlerAndRunnable> onCompletionActions) {
    for (HandlerAndRunnable pendingAction : onCompletionActions) {
      pendingAction.dispatch();
    }
    pendingOnCompletionActions.removeAll(onCompletionActions);
  }

  private void addMediaSourcesInternal(
      int index, Collection<MediaSourceHolder> mediaSourceHolders) {
    for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
      addMediaSourceInternal(index++, mediaSourceHolder);
    }
  }

  private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) {
    if (newIndex > 0) {
      MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
      Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
      newMediaSourceHolder.reset(
          newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount());
    } else {
      newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0);
    }
    Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline();
    correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount());
    mediaSourceHolders.add(newIndex, newMediaSourceHolder);
    mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder);
    prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);
    if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) {
      enabledMediaSourceHolders.add(newMediaSourceHolder);
    } else {
      disableChildSource(newMediaSourceHolder);
    }
  }

  private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
    if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) {
      MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1);
      int windowOffsetUpdate =
          timeline.getWindowCount()
              - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild);
      if (windowOffsetUpdate != 0) {
        correctOffsets(
            mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate);
      }
    }
    scheduleTimelineUpdate();
  }

  private void removeMediaSourceInternal(int index) {
    MediaSourceHolder holder = mediaSourceHolders.remove(index);
    mediaSourceByUid.remove(holder.uid);
    Timeline oldTimeline = holder.mediaSource.getTimeline();
    correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount());
    holder.isRemoved = true;
    maybeReleaseChildSource(holder);
  }

  private void moveMediaSourceInternal(int currentIndex, int newIndex) {
    int startIndex = min(currentIndex, newIndex);
    int endIndex = max(currentIndex, newIndex);
    int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
    mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));
    for (int i = startIndex; i <= endIndex; i++) {
      MediaSourceHolder holder = mediaSourceHolders.get(i);
      holder.childIndex = i;
      holder.firstWindowIndexInChild = windowOffset;
      windowOffset += holder.mediaSource.getTimeline().getWindowCount();
    }
  }

  private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) {
    // TODO: Replace window index with uid in reporting to get rid of this inefficient method and
    // the childIndex and firstWindowIndexInChild variables.
    for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
      MediaSourceHolder holder = mediaSourceHolders.get(i);
      holder.childIndex += childIndexUpdate;
      holder.firstWindowIndexInChild += windowOffsetUpdate;
    }
  }

  private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
    // Release if the source has been removed from the playlist and no periods are still active.
    if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
      enabledMediaSourceHolders.remove(mediaSourceHolder);
      releaseChildSource(mediaSourceHolder);
    }
  }

  private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
    enabledMediaSourceHolders.add(mediaSourceHolder);
    enableChildSource(mediaSourceHolder);
  }

  private void disableUnusedMediaSources() {
    Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
    while (iterator.hasNext()) {
      MediaSourceHolder holder = iterator.next();
      if (holder.activeMediaPeriodIds.isEmpty()) {
        disableChildSource(holder);
        iterator.remove();
      }
    }
  }

  /** Return uid of media source holder from period uid of concatenated source. */
  private static Object getMediaSourceHolderUid(Object periodUid) {
    return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
  }

  /** Return uid of child period from period uid of concatenated source. */
  private static Object getChildPeriodUid(Object periodUid) {
    return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
  }

  private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
    return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
  }

  /** Data class to hold playlist media sources together with meta data needed to process them. */
  /* package */ static final class MediaSourceHolder {

    public final MaskingMediaSource mediaSource;
    public final Object uid;
    public final List<MediaPeriodId> activeMediaPeriodIds;

    public int childIndex;
    public int firstWindowIndexInChild;
    public boolean isRemoved;

    public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
      this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
      this.activeMediaPeriodIds = new ArrayList<>();
      this.uid = new Object();
    }

    public void reset(int childIndex, int firstWindowIndexInChild) {
      this.childIndex = childIndex;
      this.firstWindowIndexInChild = firstWindowIndexInChild;
      this.isRemoved = false;
      this.activeMediaPeriodIds.clear();
    }
  }

  /** Message used to post actions from app thread to playback thread. */
  private static final class MessageData<T> {

    public final int index;
    public final T customData;
    @Nullable public final HandlerAndRunnable onCompletionAction;

    public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
      this.index = index;
      this.customData = customData;
      this.onCompletionAction = onCompletionAction;
    }
  }

  /** Timeline exposing concatenated timelines of playlist media sources. */
  private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {

    private final int windowCount;
    private final int periodCount;
    private final int[] firstPeriodInChildIndices;
    private final int[] firstWindowInChildIndices;
    private final Timeline[] timelines;
    private final Object[] uids;
    private final HashMap<Object, Integer> childIndexByUid;

    public ConcatenatedTimeline(
        Collection<MediaSourceHolder> mediaSourceHolders,
        ShuffleOrder shuffleOrder,
        boolean isAtomic) {
      super(isAtomic, shuffleOrder);
      int childCount = mediaSourceHolders.size();
      firstPeriodInChildIndices = new int[childCount];
      firstWindowInChildIndices = new int[childCount];
      timelines = new Timeline[childCount];
      uids = new Object[childCount];
      childIndexByUid = new HashMap<>();
      int index = 0;
      int windowCount = 0;
      int periodCount = 0;
      for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
        timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
        firstWindowInChildIndices[index] = windowCount;
        firstPeriodInChildIndices[index] = periodCount;
        windowCount += timelines[index].getWindowCount();
        periodCount += timelines[index].getPeriodCount();
        uids[index] = mediaSourceHolder.uid;
        childIndexByUid.put(uids[index], index++);
      }
      this.windowCount = windowCount;
      this.periodCount = periodCount;
    }

    @Override
    protected int getChildIndexByPeriodIndex(int periodIndex) {
      return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
    }

    @Override
    protected int getChildIndexByWindowIndex(int windowIndex) {
      return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
    }

    @Override
    protected int getChildIndexByChildUid(Object childUid) {
      @Nullable Integer index = childIndexByUid.get(childUid);
      return index == null ? C.INDEX_UNSET : index;
    }

    @Override
    protected Timeline getTimelineByChildIndex(int childIndex) {
      return timelines[childIndex];
    }

    @Override
    protected int getFirstPeriodIndexByChildIndex(int childIndex) {
      return firstPeriodInChildIndices[childIndex];
    }

    @Override
    protected int getFirstWindowIndexByChildIndex(int childIndex) {
      return firstWindowInChildIndices[childIndex];
    }

    @Override
    protected Object getChildUidByChildIndex(int childIndex) {
      return uids[childIndex];
    }

    @Override
    public int getWindowCount() {
      return windowCount;
    }

    @Override
    public int getPeriodCount() {
      return periodCount;
    }
  }

  /** A media source which does nothing and does not support creating periods. */
  private static final class FakeMediaSource extends BaseMediaSource {

    @Override
    protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
      // Do nothing.
    }

    @Override
    public MediaItem getMediaItem() {
      return EMPTY_MEDIA_ITEM;
    }

    @Override
    protected void releaseSourceInternal() {
      // Do nothing.
    }

    @Override
    public void maybeThrowSourceInfoRefreshError() {
      // Do nothing.
    }

    @Override
    public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
      throw new UnsupportedOperationException();
    }

    @Override
    public void releasePeriod(MediaPeriod mediaPeriod) {
      // Do nothing.
    }
  }

  private static final class HandlerAndRunnable {

    private final Handler handler;
    private final Runnable runnable;

    public HandlerAndRunnable(Handler handler, Runnable runnable) {
      this.handler = handler;
      this.runnable = runnable;
    }

    public void dispatch() {
      handler.post(runnable);
    }
  }
}