public abstract class

SimpleBasePlayer

extends BasePlayer

 java.lang.Object

androidx.media3.common.BasePlayer

↳androidx.media3.common.SimpleBasePlayer

Subclasses:

ForwardingSimpleBasePlayer, CompositionPlayer

Gradle dependencies

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

  • groupId: androidx.media3
  • artifactId: media3-common
  • version: 1.5.0-alpha01

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

Overview

A base implementation for Player that reduces the number of methods to implement to a minimum.

Implementation notes:

This base class handles various aspects of the player implementation to simplify the subclass:
  • The SimpleBasePlayer.State can only be created with allowed combinations of state values, avoiding any invalid player states.
  • Only functionality that is declared as available needs to be implemented. Other methods are automatically ignored.
  • Listener handling and informing listeners of state changes is handled automatically.
  • The base class provides a framework for asynchronous handling of method calls. It changes the visible playback state immediately to the most likely outcome to ensure the user-visible state changes look like synchronous operations. The state is then updated again once the asynchronous method calls have been fully handled.

Summary

Fields
from BasePlayerwindow
Constructors
protectedSimpleBasePlayer(Looper applicationLooper)

Creates the base class.

protectedSimpleBasePlayer(Looper applicationLooper, Clock clock)

Creates the base class.

Methods
public final voidaddListener(Player.Listener listener)

public final voidaddMediaItems(int index, java.util.List<MediaItem> mediaItems)

public final voidclearVideoSurface()

public final voidclearVideoSurface(Surface surface)

public final voidclearVideoSurfaceHolder(SurfaceHolder surfaceHolder)

public final voidclearVideoSurfaceView(SurfaceView surfaceView)

public final voidclearVideoTextureView(TextureView textureView)

public final voiddecreaseDeviceVolume()

public final voiddecreaseDeviceVolume(int flags)

public final LoopergetApplicationLooper()

public final AudioAttributesgetAudioAttributes()

public final Player.CommandsgetAvailableCommands()

public final longgetBufferedPosition()

public final longgetContentBufferedPosition()

public final longgetContentPosition()

public final intgetCurrentAdGroupIndex()

public final intgetCurrentAdIndexInAdGroup()

public final CueGroupgetCurrentCues()

public final intgetCurrentMediaItemIndex()

public final intgetCurrentPeriodIndex()

public final longgetCurrentPosition()

public final TimelinegetCurrentTimeline()

public final TracksgetCurrentTracks()

public final DeviceInfogetDeviceInfo()

public final intgetDeviceVolume()

public final longgetDuration()

public final longgetMaxSeekToPreviousPosition()

public final MediaMetadatagetMediaMetadata()

protected SimpleBasePlayer.MediaItemDatagetPlaceholderMediaItemData(MediaItem mediaItem)

Returns the placeholder SimpleBasePlayer.MediaItemData used for a new MediaItem added to the playlist.

protected SimpleBasePlayer.StategetPlaceholderState(SimpleBasePlayer.State suggestedPlaceholderState)

Returns the placeholder state used while a player method is handled asynchronously.

public final PlaybackParametersgetPlaybackParameters()

public final intgetPlaybackState()

public final intgetPlaybackSuppressionReason()

public final PlaybackExceptiongetPlayerError()

public final MediaMetadatagetPlaylistMetadata()

public final booleangetPlayWhenReady()

public final intgetRepeatMode()

public final longgetSeekBackIncrement()

public final longgetSeekForwardIncrement()

public final booleangetShuffleModeEnabled()

protected abstract SimpleBasePlayer.StategetState()

Returns the current SimpleBasePlayer.State of the player.

public final SizegetSurfaceSize()

public final longgetTotalBufferedDuration()

public final TrackSelectionParametersgetTrackSelectionParameters()

public final VideoSizegetVideoSize()

public final floatgetVolume()

protected <any>handleAddMediaItems(int index, java.util.List<MediaItem> mediaItems)

Handles calls to Player.addMediaItem(MediaItem) and Player.addMediaItems(List).

protected <any>handleClearVideoOutput(java.lang.Object videoOutput)

Handles calls to clear the video output.

protected <any>handleDecreaseDeviceVolume(int flags)

Handles calls to Player.decreaseDeviceVolume() and Player.decreaseDeviceVolume(int).

protected <any>handleIncreaseDeviceVolume(int flags)

Handles calls to Player.increaseDeviceVolume() and Player.increaseDeviceVolume(int).

protected <any>handleMoveMediaItems(int fromIndex, int toIndex, int newIndex)

Handles calls to Player.moveMediaItem(int, int) and Player.moveMediaItems(int, int, int).

protected <any>handlePrepare()

Handles calls to Player.prepare().

protected <any>handleRelease()

Handles calls to Player.release().

protected <any>handleRemoveMediaItems(int fromIndex, int toIndex)

Handles calls to Player.removeMediaItem(int) and Player.removeMediaItems(int, int).

protected <any>handleReplaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)

Handles calls to Player.replaceMediaItem(int, MediaItem) and Player.replaceMediaItems(int, int, List).

protected <any>handleSeek(int mediaItemIndex, long positionMs, int seekCommand)

Handles calls to Player.seekTo(long) and other seek operations (for example, Player.seekToNext()).

protected <any>handleSetAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)

Handles calls to set the audio attributes.

protected <any>handleSetDeviceMuted(boolean muted, int flags)

Handles calls to Player.setDeviceMuted(boolean) and Player.setDeviceMuted(boolean, int).

protected <any>handleSetDeviceVolume(int deviceVolume, int flags)

Handles calls to Player.setDeviceVolume(int) and Player.setDeviceVolume(int, int).

protected <any>handleSetMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)

Handles calls to Player.setMediaItem(MediaItem) and Player.setMediaItems(List).

protected <any>handleSetPlaybackParameters(PlaybackParameters playbackParameters)

Handles calls to Player.setPlaybackParameters(PlaybackParameters) or Player.setPlaybackSpeed(float).

protected <any>handleSetPlaylistMetadata(MediaMetadata playlistMetadata)

Handles calls to Player.setPlaylistMetadata(MediaMetadata).

protected <any>handleSetPlayWhenReady(boolean playWhenReady)

Handles calls to Player.setPlayWhenReady(boolean), Player.play() and Player.pause().

protected <any>handleSetRepeatMode(int repeatMode)

Handles calls to Player.setRepeatMode(int).

protected <any>handleSetShuffleModeEnabled(boolean shuffleModeEnabled)

Handles calls to Player.setShuffleModeEnabled(boolean).

protected <any>handleSetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters)

Handles calls to Player.setTrackSelectionParameters(TrackSelectionParameters).

protected <any>handleSetVideoOutput(java.lang.Object videoOutput)

Handles calls to set the video output.

protected <any>handleSetVolume(float volume)

Handles calls to Player.setVolume(float).

protected <any>handleStop()

Handles calls to Player.stop().

public final voidincreaseDeviceVolume()

public final voidincreaseDeviceVolume(int flags)

protected final voidinvalidateState()

Invalidates the current state.

public final booleanisDeviceMuted()

public final booleanisLoading()

public final booleanisPlayingAd()

public final voidmoveMediaItems(int fromIndex, int toIndex, int newIndex)

public final voidprepare()

public final voidrelease()

public final voidremoveListener(Player.Listener listener)

public final voidremoveMediaItems(int fromIndex, int toIndex)

public final voidreplaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)

public abstract voidseekTo(int mediaItemIndex, long positionMs, int seekCommand, boolean isRepeatingCurrentItem)

Seeks to a position in the specified MediaItem.

public final voidsetAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)

public final voidsetDeviceMuted(boolean muted)

public final voidsetDeviceMuted(boolean muted, int flags)

public final voidsetDeviceVolume(int volume)

public final voidsetDeviceVolume(int volume, int flags)

public final voidsetMediaItems(java.util.List<MediaItem> mediaItems, boolean resetPosition)

public final voidsetMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)

public final voidsetPlaybackParameters(PlaybackParameters playbackParameters)

public final voidsetPlaylistMetadata(MediaMetadata mediaMetadata)

public final voidsetPlayWhenReady(boolean playWhenReady)

public final voidsetRepeatMode(int repeatMode)

public final voidsetShuffleModeEnabled(boolean shuffleModeEnabled)

public final voidsetTrackSelectionParameters(TrackSelectionParameters parameters)

public final voidsetVideoSurface(Surface surface)

public final voidsetVideoSurfaceHolder(SurfaceHolder surfaceHolder)

public final voidsetVideoSurfaceView(SurfaceView surfaceView)

public final voidsetVideoTextureView(TextureView textureView)

public final voidsetVolume(float volume)

public final voidstop()

protected final voidverifyApplicationThread()

Throws an java.lang.IllegalStateException if the the thread calling this method does not match the thread that was specified upon construction of this instance.

from BasePlayeraddMediaItem, addMediaItem, addMediaItems, canAdvertiseSession, clearMediaItems, getBufferedPercentage, getContentDuration, getCurrentLiveOffset, getCurrentManifest, getCurrentMediaItem, getCurrentWindowIndex, getMediaItemAt, getMediaItemCount, getNextMediaItemIndex, getNextWindowIndex, getPreviousMediaItemIndex, getPreviousWindowIndex, hasNext, hasNextMediaItem, hasNextWindow, hasPreviousMediaItem, isCommandAvailable, isCurrentMediaItemDynamic, isCurrentMediaItemLive, isCurrentMediaItemSeekable, isCurrentWindowDynamic, isCurrentWindowLive, isCurrentWindowSeekable, isPlaying, moveMediaItem, next, pause, play, removeMediaItem, replaceMediaItem, seekBack, seekForward, seekTo, seekTo, seekToDefaultPosition, seekToDefaultPosition, seekToNext, seekToNextMediaItem, seekToNextWindow, seekToPrevious, seekToPreviousMediaItem, seekToPreviousWindow, setMediaItem, setMediaItem, setMediaItem, setMediaItems, setPlaybackSpeed
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

protected SimpleBasePlayer(Looper applicationLooper)

Creates the base class.

Parameters:

applicationLooper: The that must be used for all calls to the player and that is used to call listeners on.

protected SimpleBasePlayer(Looper applicationLooper, Clock clock)

Creates the base class.

Parameters:

applicationLooper: The that must be used for all calls to the player and that is used to call listeners on.
clock: The Clock that will be used by the player.

Methods

public final void addListener(Player.Listener listener)

public final void removeListener(Player.Listener listener)

public final Looper getApplicationLooper()

public final Player.Commands getAvailableCommands()

public final void setPlayWhenReady(boolean playWhenReady)

public final boolean getPlayWhenReady()

public final void setMediaItems(java.util.List<MediaItem> mediaItems, boolean resetPosition)

public final void setMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)

public final void addMediaItems(int index, java.util.List<MediaItem> mediaItems)

public final void moveMediaItems(int fromIndex, int toIndex, int newIndex)

public final void replaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)

public final void removeMediaItems(int fromIndex, int toIndex)

public final void prepare()

public final int getPlaybackState()

public final int getPlaybackSuppressionReason()

public final PlaybackException getPlayerError()

public final void setRepeatMode(int repeatMode)

public final int getRepeatMode()

public final void setShuffleModeEnabled(boolean shuffleModeEnabled)

public final boolean getShuffleModeEnabled()

public final boolean isLoading()

public abstract void seekTo(int mediaItemIndex, long positionMs, int seekCommand, boolean isRepeatingCurrentItem)

Seeks to a position in the specified MediaItem.

Parameters:

mediaItemIndex: The index of the MediaItem. If the original seek operation did not directly specify an index, this is the most likely implied index based on the available player state. If the implied action is to do nothing, this will be C.INDEX_UNSET.
positionMs: The seek position in the specified MediaItem in milliseconds, or C.TIME_UNSET to seek to the media item's default position. If the original seek operation did not directly specify a position, this is the most likely implied position based on the available player state.
seekCommand: The Player.Command used to trigger the seek.
isRepeatingCurrentItem: Whether this seeks repeats the current item.

public final long getSeekBackIncrement()

public final long getSeekForwardIncrement()

public final long getMaxSeekToPreviousPosition()

public final void setPlaybackParameters(PlaybackParameters playbackParameters)

public final PlaybackParameters getPlaybackParameters()

public final void stop()

public final void release()

public final Tracks getCurrentTracks()

public final TrackSelectionParameters getTrackSelectionParameters()

public final void setTrackSelectionParameters(TrackSelectionParameters parameters)

public final MediaMetadata getMediaMetadata()

public final MediaMetadata getPlaylistMetadata()

public final void setPlaylistMetadata(MediaMetadata mediaMetadata)

public final Timeline getCurrentTimeline()

public final int getCurrentPeriodIndex()

public final int getCurrentMediaItemIndex()

public final long getDuration()

public final long getCurrentPosition()

public final long getBufferedPosition()

public final long getTotalBufferedDuration()

public final boolean isPlayingAd()

public final int getCurrentAdGroupIndex()

public final int getCurrentAdIndexInAdGroup()

public final long getContentPosition()

public final long getContentBufferedPosition()

public final AudioAttributes getAudioAttributes()

public final void setVolume(float volume)

public final float getVolume()

public final void setVideoSurface(Surface surface)

public final void setVideoSurfaceHolder(SurfaceHolder surfaceHolder)

public final void setVideoSurfaceView(SurfaceView surfaceView)

public final void setVideoTextureView(TextureView textureView)

public final void clearVideoSurface()

public final void clearVideoSurface(Surface surface)

public final void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder)

public final void clearVideoSurfaceView(SurfaceView surfaceView)

public final void clearVideoTextureView(TextureView textureView)

public final VideoSize getVideoSize()

public final Size getSurfaceSize()

public final CueGroup getCurrentCues()

public final DeviceInfo getDeviceInfo()

public final int getDeviceVolume()

public final boolean isDeviceMuted()

public final void setDeviceVolume(int volume)

Deprecated: Use SimpleBasePlayer.setDeviceVolume(int, int) instead.

public final void setDeviceVolume(int volume, int flags)

public final void increaseDeviceVolume()

Deprecated: Use SimpleBasePlayer.increaseDeviceVolume(int) instead.

public final void increaseDeviceVolume(int flags)

public final void decreaseDeviceVolume()

Deprecated: Use SimpleBasePlayer.decreaseDeviceVolume(int) instead.

public final void decreaseDeviceVolume(int flags)

public final void setDeviceMuted(boolean muted)

Deprecated: Use SimpleBasePlayer.setDeviceMuted(boolean, int) instead.

public final void setDeviceMuted(boolean muted, int flags)

public final void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)

protected final void invalidateState()

Invalidates the current state.

Triggers a call to SimpleBasePlayer.getState() and informs listeners if the state changed.

Note that this may not have an immediate effect while there are still player methods being handled asynchronously. The state will be invalidated automatically once these pending synchronous operations are finished and there is no need to call this method again.

protected abstract SimpleBasePlayer.State getState()

Returns the current SimpleBasePlayer.State of the player.

The SimpleBasePlayer.State should include all available commands indicating which player methods are allowed to be called.

Note that this method won't be called while asynchronous handling of player methods is in progress. This means that the implementation doesn't need to handle state changes caused by these asynchronous operations until they are done and can return the currently known state directly. The placeholder state used while these asynchronous operations are in progress can be customized by overriding SimpleBasePlayer.getPlaceholderState(SimpleBasePlayer.State) if required.

protected SimpleBasePlayer.State getPlaceholderState(SimpleBasePlayer.State suggestedPlaceholderState)

Returns the placeholder state used while a player method is handled asynchronously.

The suggestedPlaceholderState already contains the most likely state update, for example setting SimpleBasePlayer.State.playWhenReady to true if player.setPlayWhenReady(true) is called, and an implementations only needs to override this method if it can determine a more accurate placeholder state.

Parameters:

suggestedPlaceholderState: The suggested placeholder SimpleBasePlayer.State, including the most likely outcome of handling all pending asynchronous operations.

Returns:

The placeholder SimpleBasePlayer.State to use while asynchronous operations are pending.

protected SimpleBasePlayer.MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem)

Returns the placeholder SimpleBasePlayer.MediaItemData used for a new MediaItem added to the playlist.

An implementation only needs to override this method if it can determine a more accurate placeholder state than the default.

Parameters:

mediaItem: The MediaItem added to the playlist.

Returns:

The SimpleBasePlayer.MediaItemData used as placeholder while adding the item to the playlist is in progress.

protected <any> handleSetPlayWhenReady(boolean playWhenReady)

Handles calls to Player.setPlayWhenReady(boolean), Player.play() and Player.pause().

Will only be called if Player.COMMAND_PLAY_PAUSE is available.

Parameters:

playWhenReady: The requested SimpleBasePlayer.State.playWhenReady

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handlePrepare()

Handles calls to Player.prepare().

Will only be called if Player.COMMAND_PREPARE is available.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleStop()

Handles calls to Player.stop().

Will only be called if Player.COMMAND_STOP is available.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleRelease()

Handles calls to Player.release().

Will only be called if Player.COMMAND_RELEASE is available.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetRepeatMode(int repeatMode)

Handles calls to Player.setRepeatMode(int).

Will only be called if Player.COMMAND_SET_REPEAT_MODE is available.

Parameters:

repeatMode: The requested .

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetShuffleModeEnabled(boolean shuffleModeEnabled)

Handles calls to Player.setShuffleModeEnabled(boolean).

Will only be called if Player.COMMAND_SET_SHUFFLE_MODE is available.

Parameters:

shuffleModeEnabled: Whether shuffle mode was requested to be enabled.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetPlaybackParameters(PlaybackParameters playbackParameters)

Handles calls to Player.setPlaybackParameters(PlaybackParameters) or Player.setPlaybackSpeed(float).

Will only be called if Player.COMMAND_SET_SPEED_AND_PITCH is available.

Parameters:

playbackParameters: The requested PlaybackParameters.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetTrackSelectionParameters(TrackSelectionParameters trackSelectionParameters)

Handles calls to Player.setTrackSelectionParameters(TrackSelectionParameters).

Will only be called if Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS is available.

Parameters:

trackSelectionParameters: The requested TrackSelectionParameters.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetPlaylistMetadata(MediaMetadata playlistMetadata)

Handles calls to Player.setPlaylistMetadata(MediaMetadata).

Will only be called if Player.COMMAND_SET_PLAYLIST_METADATA is available.

Parameters:

playlistMetadata: The requested playlist metadata.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetVolume(float volume)

Handles calls to Player.setVolume(float).

Will only be called if Player.COMMAND_SET_VOLUME is available.

Parameters:

volume: The requested audio volume, with 0 being silence and 1 being unity gain (signal unchanged).

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetDeviceVolume(int deviceVolume, int flags)

Handles calls to Player.setDeviceVolume(int) and Player.setDeviceVolume(int, int).

Will only be called if Player.COMMAND_SET_DEVICE_VOLUME or Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS is available.

Parameters:

deviceVolume: The requested device volume.
flags: Either 0 or a bitwise combination of one or more C.VolumeFlags.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleIncreaseDeviceVolume(int flags)

Handles calls to Player.increaseDeviceVolume() and Player.increaseDeviceVolume(int).

Will only be called if Player.COMMAND_ADJUST_DEVICE_VOLUME or Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS is available.

Parameters:

flags: Either 0 or a bitwise combination of one or more C.VolumeFlags.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleDecreaseDeviceVolume(int flags)

Handles calls to Player.decreaseDeviceVolume() and Player.decreaseDeviceVolume(int).

Will only be called if Player.COMMAND_ADJUST_DEVICE_VOLUME or Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS is available.

Parameters:

flags: Either 0 or a bitwise combination of one or more C.VolumeFlags.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetDeviceMuted(boolean muted, int flags)

Handles calls to Player.setDeviceMuted(boolean) and Player.setDeviceMuted(boolean, int).

Will only be called if Player.COMMAND_ADJUST_DEVICE_VOLUME or Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS is available.

Parameters:

muted: Whether the device was requested to be muted.
flags: Either 0 or a bitwise combination of one or more C.VolumeFlags.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus)

Handles calls to set the audio attributes.

Will only be called if Player.COMMAND_SET_AUDIO_ATTRIBUTES is available.

Parameters:

audioAttributes: The attributes to use for audio playback.
handleAudioFocus: True if the player should handle audio focus, false otherwise.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetVideoOutput(java.lang.Object videoOutput)

Handles calls to set the video output.

Will only be called if Player.COMMAND_SET_VIDEO_SURFACE is available.

Parameters:

videoOutput: The requested video output. This is either a , SurfaceHolder, TextureView or .

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleClearVideoOutput(java.lang.Object videoOutput)

Handles calls to clear the video output.

Will only be called if Player.COMMAND_SET_VIDEO_SURFACE is available.

Parameters:

videoOutput: The video output to clear. If null any current output should be cleared. If non-null, the output should only be cleared if it matches the provided argument. This is either a , SurfaceHolder, TextureView or .

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSetMediaItems(java.util.List<MediaItem> mediaItems, int startIndex, long startPositionMs)

Handles calls to Player.setMediaItem(MediaItem) and Player.setMediaItems(List).

Will only be called if Player.COMMAND_SET_MEDIA_ITEM or Player.COMMAND_CHANGE_MEDIA_ITEMS is available. If only Player.COMMAND_SET_MEDIA_ITEM is available, the list of media items will always contain exactly one item.

Parameters:

mediaItems: The media items to add.
startIndex: The index at which to start playback from, or C.INDEX_UNSET to start at the default item.
startPositionMs: The position in milliseconds to start playback from, or C.TIME_UNSET to start at the default position in the media item.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleAddMediaItems(int index, java.util.List<MediaItem> mediaItems)

Handles calls to Player.addMediaItem(MediaItem) and Player.addMediaItems(List).

Will only be called if Player.COMMAND_CHANGE_MEDIA_ITEMS is available.

Parameters:

index: The index at which to add the items. The index is in the range 0 <= index <= BasePlayer.getMediaItemCount().
mediaItems: The media items to add.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleMoveMediaItems(int fromIndex, int toIndex, int newIndex)

Handles calls to Player.moveMediaItem(int, int) and Player.moveMediaItems(int, int, int).

Will only be called if Player.COMMAND_CHANGE_MEDIA_ITEMS is available.

Parameters:

fromIndex: The start index of the items to move. The index is in the range 0 <= fromIndex < BasePlayer.getMediaItemCount().
toIndex: The index of the first item not to be included in the move (exclusive). The index is in the range fromIndex < toIndex <= BasePlayer.getMediaItemCount().
newIndex: The new index of the first moved item. The index is in the range 0 <= newIndex < - (toIndex - fromIndex).

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleReplaceMediaItems(int fromIndex, int toIndex, java.util.List<MediaItem> mediaItems)

Handles calls to Player.replaceMediaItem(int, MediaItem) and Player.replaceMediaItems(int, int, List).

Will only be called if Player.COMMAND_CHANGE_MEDIA_ITEMS is available.

Parameters:

fromIndex: The start index of the items to replace. The index is in the range 0 <= fromIndex < BasePlayer.getMediaItemCount().
toIndex: The index of the first item not to be replaced (exclusive). The index is in the range fromIndex < toIndex <= BasePlayer.getMediaItemCount().
mediaItems: The media items to replace the specified range with.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleRemoveMediaItems(int fromIndex, int toIndex)

Handles calls to Player.removeMediaItem(int) and Player.removeMediaItems(int, int).

Will only be called if Player.COMMAND_CHANGE_MEDIA_ITEMS is available.

Parameters:

fromIndex: The index at which to start removing media items. The index is in the range 0 <= fromIndex < BasePlayer.getMediaItemCount().
toIndex: The index of the first item to be kept (exclusive). The index is in the range fromIndex < toIndex <= BasePlayer.getMediaItemCount().

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected <any> handleSeek(int mediaItemIndex, long positionMs, int seekCommand)

Handles calls to Player.seekTo(long) and other seek operations (for example, Player.seekToNext()).

Will only be called if the appropriate Player.Command, for example Player.COMMAND_SEEK_TO_MEDIA_ITEM or Player.COMMAND_SEEK_TO_NEXT, is available.

Parameters:

mediaItemIndex: The media item index to seek to. If the original seek operation did not directly specify an index, this is the most likely implied index based on the available player state. If the implied action is to do nothing, this will be C.INDEX_UNSET.
positionMs: The position in milliseconds to start playback from, or C.TIME_UNSET to start at the default position in the media item. If the original seek operation did not directly specify a position, this is the most likely implied position based on the available player state.
seekCommand: The Player.Command used to trigger the seek.

Returns:

A indicating the completion of all immediate SimpleBasePlayer.State changes caused by this call.

protected final void verifyApplicationThread()

Throws an java.lang.IllegalStateException if the the thread calling this method does not match the thread that was specified upon construction of this instance.

Subclasses can use this method to verify that their own defined methods are also accessed by the correct thread.

Source

/*
 * Copyright 2022 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.common;

import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.common.util.Util.usToMs;
import static java.lang.Math.max;
import static java.lang.Math.min;

import android.graphics.Rect;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * A base implementation for {@link Player} that reduces the number of methods to implement to a
 * minimum.
 *
 * <p>Implementation notes:
 *
 * <ul>
 *   <li>Subclasses must override {@link #getState()} to populate the current player state on
 *       request.
 *   <li>The {@link State} should set the {@linkplain State.Builder#setAvailableCommands available
 *       commands} to indicate which {@link Player} methods are supported.
 *   <li>All setter-like player methods (for example, {@link #setPlayWhenReady}) forward to
 *       overridable methods (for example, {@link #handleSetPlayWhenReady}) that can be used to
 *       handle these requests. These methods return a {@link ListenableFuture} to indicate when the
 *       request has been handled and is fully reflected in the values returned from {@link
 *       #getState}. This class will automatically request a state update once the request is done.
 *       If the state changes can be handled synchronously, these methods can return Guava's {@link
 *       Futures#immediateVoidFuture()}.
 *   <li>Subclasses can manually trigger state updates with {@link #invalidateState}, for example if
 *       something changes independent of {@link Player} method calls.
 * </ul>
 *
 * This base class handles various aspects of the player implementation to simplify the subclass:
 *
 * <ul>
 *   <li>The {@link State} can only be created with allowed combinations of state values, avoiding
 *       any invalid player states.
 *   <li>Only functionality that is declared as {@linkplain Player.Command available} needs to be
 *       implemented. Other methods are automatically ignored.
 *   <li>Listener handling and informing listeners of state changes is handled automatically.
 *   <li>The base class provides a framework for asynchronous handling of method calls. It changes
 *       the visible playback state immediately to the most likely outcome to ensure the
 *       user-visible state changes look like synchronous operations. The state is then updated
 *       again once the asynchronous method calls have been fully handled.
 * </ul>
 */
@UnstableApi
public abstract class SimpleBasePlayer extends BasePlayer {

  /** An immutable state description of the player. */
  protected static final class State {

    /** A builder for {@link State} objects. */
    public static final class Builder {

      private Commands availableCommands;
      private boolean playWhenReady;
      private @PlayWhenReadyChangeReason int playWhenReadyChangeReason;
      private @Player.State int playbackState;
      private @PlaybackSuppressionReason int playbackSuppressionReason;
      @Nullable private PlaybackException playerError;
      private @RepeatMode int repeatMode;
      private boolean shuffleModeEnabled;
      private boolean isLoading;
      private long seekBackIncrementMs;
      private long seekForwardIncrementMs;
      private long maxSeekToPreviousPositionMs;
      private PlaybackParameters playbackParameters;
      private TrackSelectionParameters trackSelectionParameters;
      private AudioAttributes audioAttributes;
      private float volume;
      private VideoSize videoSize;
      private CueGroup currentCues;
      private DeviceInfo deviceInfo;
      private int deviceVolume;
      private boolean isDeviceMuted;
      private Size surfaceSize;
      private boolean newlyRenderedFirstFrame;
      private Metadata timedMetadata;
      @Nullable private ImmutableList<MediaItemData> playlist;
      private Timeline timeline;
      @Nullable private Tracks currentTracks;
      @Nullable private MediaMetadata currentMetadata;
      private MediaMetadata playlistMetadata;
      private int currentMediaItemIndex;
      private int currentAdGroupIndex;
      private int currentAdIndexInAdGroup;
      @Nullable private Long contentPositionMs;
      private PositionSupplier contentPositionMsSupplier;
      @Nullable private Long adPositionMs;
      private PositionSupplier adPositionMsSupplier;
      private PositionSupplier contentBufferedPositionMsSupplier;
      private PositionSupplier adBufferedPositionMsSupplier;
      private PositionSupplier totalBufferedDurationMsSupplier;
      private boolean hasPositionDiscontinuity;
      private @Player.DiscontinuityReason int positionDiscontinuityReason;
      private long discontinuityPositionMs;

      /** Creates the builder. */
      public Builder() {
        availableCommands = Commands.EMPTY;
        playWhenReady = false;
        playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
        playbackState = Player.STATE_IDLE;
        playbackSuppressionReason = Player.PLAYBACK_SUPPRESSION_REASON_NONE;
        playerError = null;
        repeatMode = Player.REPEAT_MODE_OFF;
        shuffleModeEnabled = false;
        isLoading = false;
        seekBackIncrementMs = C.DEFAULT_SEEK_BACK_INCREMENT_MS;
        seekForwardIncrementMs = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS;
        maxSeekToPreviousPositionMs = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
        playbackParameters = PlaybackParameters.DEFAULT;
        trackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
        audioAttributes = AudioAttributes.DEFAULT;
        volume = 1f;
        videoSize = VideoSize.UNKNOWN;
        currentCues = CueGroup.EMPTY_TIME_ZERO;
        deviceInfo = DeviceInfo.UNKNOWN;
        deviceVolume = 0;
        isDeviceMuted = false;
        surfaceSize = Size.UNKNOWN;
        newlyRenderedFirstFrame = false;
        timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
        playlist = ImmutableList.of();
        timeline = Timeline.EMPTY;
        currentTracks = null;
        currentMetadata = null;
        playlistMetadata = MediaMetadata.EMPTY;
        currentMediaItemIndex = C.INDEX_UNSET;
        currentAdGroupIndex = C.INDEX_UNSET;
        currentAdIndexInAdGroup = C.INDEX_UNSET;
        contentPositionMs = null;
        contentPositionMsSupplier = PositionSupplier.getConstant(C.TIME_UNSET);
        adPositionMs = null;
        adPositionMsSupplier = PositionSupplier.ZERO;
        contentBufferedPositionMsSupplier = PositionSupplier.getConstant(C.TIME_UNSET);
        adBufferedPositionMsSupplier = PositionSupplier.ZERO;
        totalBufferedDurationMsSupplier = PositionSupplier.ZERO;
        hasPositionDiscontinuity = false;
        positionDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL;
        discontinuityPositionMs = 0;
      }

      private Builder(State state) {
        this.availableCommands = state.availableCommands;
        this.playWhenReady = state.playWhenReady;
        this.playWhenReadyChangeReason = state.playWhenReadyChangeReason;
        this.playbackState = state.playbackState;
        this.playbackSuppressionReason = state.playbackSuppressionReason;
        this.playerError = state.playerError;
        this.repeatMode = state.repeatMode;
        this.shuffleModeEnabled = state.shuffleModeEnabled;
        this.isLoading = state.isLoading;
        this.seekBackIncrementMs = state.seekBackIncrementMs;
        this.seekForwardIncrementMs = state.seekForwardIncrementMs;
        this.maxSeekToPreviousPositionMs = state.maxSeekToPreviousPositionMs;
        this.playbackParameters = state.playbackParameters;
        this.trackSelectionParameters = state.trackSelectionParameters;
        this.audioAttributes = state.audioAttributes;
        this.volume = state.volume;
        this.videoSize = state.videoSize;
        this.currentCues = state.currentCues;
        this.deviceInfo = state.deviceInfo;
        this.deviceVolume = state.deviceVolume;
        this.isDeviceMuted = state.isDeviceMuted;
        this.surfaceSize = state.surfaceSize;
        this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame;
        this.timedMetadata = state.timedMetadata;
        this.timeline = state.timeline;
        if (state.timeline instanceof PlaylistTimeline) {
          this.playlist = ((PlaylistTimeline) state.timeline).playlist;
        } else {
          this.currentTracks = state.currentTracks;
          this.currentMetadata = state.currentMetadata;
        }
        this.playlistMetadata = state.playlistMetadata;
        this.currentMediaItemIndex = state.currentMediaItemIndex;
        this.currentAdGroupIndex = state.currentAdGroupIndex;
        this.currentAdIndexInAdGroup = state.currentAdIndexInAdGroup;
        this.contentPositionMs = null;
        this.contentPositionMsSupplier = state.contentPositionMsSupplier;
        this.adPositionMs = null;
        this.adPositionMsSupplier = state.adPositionMsSupplier;
        this.contentBufferedPositionMsSupplier = state.contentBufferedPositionMsSupplier;
        this.adBufferedPositionMsSupplier = state.adBufferedPositionMsSupplier;
        this.totalBufferedDurationMsSupplier = state.totalBufferedDurationMsSupplier;
        this.hasPositionDiscontinuity = state.hasPositionDiscontinuity;
        this.positionDiscontinuityReason = state.positionDiscontinuityReason;
        this.discontinuityPositionMs = state.discontinuityPositionMs;
      }

      /**
       * Sets the available {@link Commands}.
       *
       * @param availableCommands The available {@link Commands}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAvailableCommands(Commands availableCommands) {
        this.availableCommands = availableCommands;
        return this;
      }

      /**
       * Sets whether playback should proceed when ready and not suppressed.
       *
       * @param playWhenReady Whether playback should proceed when ready and not suppressed.
       * @param playWhenReadyChangeReason The {@linkplain PlayWhenReadyChangeReason reason} for
       *     changing the value.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlayWhenReady(
          boolean playWhenReady, @PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
        this.playWhenReady = playWhenReady;
        this.playWhenReadyChangeReason = playWhenReadyChangeReason;
        return this;
      }

      /**
       * Sets the {@linkplain Player.State state} of the player.
       *
       * <p>If the {@linkplain #setPlaylist playlist} is empty, the state must be either {@link
       * Player#STATE_IDLE} or {@link Player#STATE_ENDED}.
       *
       * @param playbackState The {@linkplain Player.State state} of the player.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaybackState(@Player.State int playbackState) {
        this.playbackState = playbackState;
        return this;
      }

      /**
       * Sets the reason why playback is suppressed even if {@link #getPlayWhenReady()} is true.
       *
       * @param playbackSuppressionReason The {@link Player.PlaybackSuppressionReason} why playback
       *     is suppressed even if {@link #getPlayWhenReady()} is true.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaybackSuppressionReason(
          @Player.PlaybackSuppressionReason int playbackSuppressionReason) {
        this.playbackSuppressionReason = playbackSuppressionReason;
        return this;
      }

      /**
       * Sets last error that caused playback to fail, or null if there was no error.
       *
       * <p>The {@linkplain #setPlaybackState playback state} must be set to {@link
       * Player#STATE_IDLE} while an error is set.
       *
       * @param playerError The last error that caused playback to fail, or null if there was no
       *     error.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlayerError(@Nullable PlaybackException playerError) {
        this.playerError = playerError;
        return this;
      }

      /**
       * Sets the {@link RepeatMode} used for playback.
       *
       * @param repeatMode The {@link RepeatMode} used for playback.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setRepeatMode(@Player.RepeatMode int repeatMode) {
        this.repeatMode = repeatMode;
        return this;
      }

      /**
       * Sets whether shuffling of media items is enabled.
       *
       * @param shuffleModeEnabled Whether shuffling of media items is enabled.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) {
        this.shuffleModeEnabled = shuffleModeEnabled;
        return this;
      }

      /**
       * Sets whether the player is currently loading its source.
       *
       * <p>The player can not be marked as loading if the {@linkplain #setPlaybackState state} is
       * {@link Player#STATE_IDLE} or {@link Player#STATE_ENDED}.
       *
       * @param isLoading Whether the player is currently loading its source.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsLoading(boolean isLoading) {
        this.isLoading = isLoading;
        return this;
      }

      /**
       * Sets the {@link Player#seekBack()} increment in milliseconds.
       *
       * @param seekBackIncrementMs The {@link Player#seekBack()} increment in milliseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setSeekBackIncrementMs(long seekBackIncrementMs) {
        this.seekBackIncrementMs = seekBackIncrementMs;
        return this;
      }

      /**
       * Sets the {@link Player#seekForward()} increment in milliseconds.
       *
       * @param seekForwardIncrementMs The {@link Player#seekForward()} increment in milliseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setSeekForwardIncrementMs(long seekForwardIncrementMs) {
        this.seekForwardIncrementMs = seekForwardIncrementMs;
        return this;
      }

      /**
       * Sets the maximum position for which {@link #seekToPrevious()} seeks to the previous item,
       * in milliseconds.
       *
       * @param maxSeekToPreviousPositionMs The maximum position for which {@link #seekToPrevious()}
       *     seeks to the previous item, in milliseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setMaxSeekToPreviousPositionMs(long maxSeekToPreviousPositionMs) {
        this.maxSeekToPreviousPositionMs = maxSeekToPreviousPositionMs;
        return this;
      }

      /**
       * Sets the currently active {@link PlaybackParameters}.
       *
       * @param playbackParameters The currently active {@link PlaybackParameters}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaybackParameters(PlaybackParameters playbackParameters) {
        this.playbackParameters = playbackParameters;
        return this;
      }

      /**
       * Sets the currently active {@link TrackSelectionParameters}.
       *
       * @param trackSelectionParameters The currently active {@link TrackSelectionParameters}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setTrackSelectionParameters(
          TrackSelectionParameters trackSelectionParameters) {
        this.trackSelectionParameters = trackSelectionParameters;
        return this;
      }

      /**
       * Sets the current {@link AudioAttributes}.
       *
       * @param audioAttributes The current {@link AudioAttributes}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAudioAttributes(AudioAttributes audioAttributes) {
        this.audioAttributes = audioAttributes;
        return this;
      }

      /**
       * Sets the current audio volume, with 0 being silence and 1 being unity gain (signal
       * unchanged).
       *
       * @param volume The current audio volume, with 0 being silence and 1 being unity gain (signal
       *     unchanged).
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setVolume(@FloatRange(from = 0, to = 1.0) float volume) {
        checkArgument(volume >= 0.0f && volume <= 1.0f);
        this.volume = volume;
        return this;
      }

      /**
       * Sets the current video size.
       *
       * @param videoSize The current video size.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setVideoSize(VideoSize videoSize) {
        this.videoSize = videoSize;
        return this;
      }

      /**
       * Sets the current {@linkplain CueGroup cues}.
       *
       * @param currentCues The current {@linkplain CueGroup cues}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setCurrentCues(CueGroup currentCues) {
        this.currentCues = currentCues;
        return this;
      }

      /**
       * Sets the {@link DeviceInfo}.
       *
       * @param deviceInfo The {@link DeviceInfo}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setDeviceInfo(DeviceInfo deviceInfo) {
        this.deviceInfo = deviceInfo;
        return this;
      }

      /**
       * Sets the current device volume.
       *
       * @param deviceVolume The current device volume.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setDeviceVolume(@IntRange(from = 0) int deviceVolume) {
        checkArgument(deviceVolume >= 0);
        this.deviceVolume = deviceVolume;
        return this;
      }

      /**
       * Sets whether the device is muted.
       *
       * @param isDeviceMuted Whether the device is muted.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsDeviceMuted(boolean isDeviceMuted) {
        this.isDeviceMuted = isDeviceMuted;
        return this;
      }

      /**
       * Sets the size of the surface onto which the video is being rendered.
       *
       * @param surfaceSize The surface size. Dimensions may be {@link C#LENGTH_UNSET} if unknown,
       *     or 0 if the video is not rendered onto a surface.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setSurfaceSize(Size surfaceSize) {
        this.surfaceSize = surfaceSize;
        return this;
      }

      /**
       * Sets whether a frame has been rendered for the first time since setting the surface, a
       * rendering reset, or since the stream being rendered was changed.
       *
       * <p>Note: As this will trigger a {@link Listener#onRenderedFirstFrame()} event, the flag
       * should only be set for the first {@link State} update after the first frame was rendered.
       *
       * @param newlyRenderedFirstFrame Whether the first frame was newly rendered.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setNewlyRenderedFirstFrame(boolean newlyRenderedFirstFrame) {
        this.newlyRenderedFirstFrame = newlyRenderedFirstFrame;
        return this;
      }

      /**
       * Sets the most recent timed {@link Metadata}.
       *
       * <p>Metadata with a {@link Metadata#presentationTimeUs} of {@link C#TIME_UNSET} will not be
       * forwarded to listeners.
       *
       * @param timedMetadata The most recent timed {@link Metadata}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setTimedMetadata(Metadata timedMetadata) {
        this.timedMetadata = timedMetadata;
        return this;
      }

      /**
       * Sets the playlist as a list of {@link MediaItemData media items}.
       *
       * <p>All items must have unique {@linkplain MediaItemData.Builder#setUid UIDs}.
       *
       * <p>This call replaces any previous playlist set via {@link #setPlaylist(Timeline, Tracks,
       * MediaMetadata)}.
       *
       * @param playlist The list of {@link MediaItemData media items} in the playlist.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaylist(List<MediaItemData> playlist) {
        HashSet<Object> uids = new HashSet<>();
        for (int i = 0; i < playlist.size(); i++) {
          checkArgument(uids.add(playlist.get(i).uid), "Duplicate MediaItemData UID in playlist");
        }
        this.playlist = ImmutableList.copyOf(playlist);
        this.timeline = new PlaylistTimeline(this.playlist);
        this.currentTracks = null;
        this.currentMetadata = null;
        return this;
      }

      /**
       * Sets the playlist as a {@link Timeline} with information about the current {@link Tracks}
       * and {@link MediaMetadata}.
       *
       * <p>This call replaces any previous playlist set via {@link #setPlaylist(List)}.
       *
       * @param timeline The {@link Timeline} containing the playlist data.
       * @param currentTracks The {@link Tracks} of the {@linkplain #setCurrentMediaItemIndex
       *     current media item}.
       * @param currentMetadata The combined {@link MediaMetadata} of the {@linkplain
       *     #setCurrentMediaItemIndex current media item}. If null, the current metadata is assumed
       *     to be the combination of the {@link MediaItem#mediaMetadata MediaItem} metadata and the
       *     metadata of the selected {@link Format#metadata Formats}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaylist(
          Timeline timeline, Tracks currentTracks, @Nullable MediaMetadata currentMetadata) {
        this.playlist = null;
        this.timeline = timeline;
        this.currentTracks = currentTracks;
        this.currentMetadata = currentMetadata;
        return this;
      }

      /**
       * Sets the playlist {@link MediaMetadata}.
       *
       * @param playlistMetadata The playlist {@link MediaMetadata}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPlaylistMetadata(MediaMetadata playlistMetadata) {
        this.playlistMetadata = playlistMetadata;
        return this;
      }

      /**
       * Sets the current media item index.
       *
       * <p>The media item index must be less than the number of {@linkplain #setPlaylist media
       * items in the playlist}, if set.
       *
       * @param currentMediaItemIndex The current media item index, or {@link C#INDEX_UNSET} to
       *     assume the default first item in the playlist.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setCurrentMediaItemIndex(int currentMediaItemIndex) {
        this.currentMediaItemIndex = currentMediaItemIndex;
        return this;
      }

      /**
       * Sets the current ad indices, or {@link C#INDEX_UNSET} if no ad is playing.
       *
       * <p>Either both indices need to be {@link C#INDEX_UNSET} or both are not {@link
       * C#INDEX_UNSET}.
       *
       * <p>Ads indices can only be set if there is a corresponding {@link AdPlaybackState} defined
       * in the current {@linkplain MediaItemData.Builder#setPeriods period}.
       *
       * @param adGroupIndex The current ad group index, or {@link C#INDEX_UNSET} if no ad is
       *     playing.
       * @param adIndexInAdGroup The current ad index in the ad group, or {@link C#INDEX_UNSET} if
       *     no ad is playing.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setCurrentAd(int adGroupIndex, int adIndexInAdGroup) {
        checkArgument((adGroupIndex == C.INDEX_UNSET) == (adIndexInAdGroup == C.INDEX_UNSET));
        this.currentAdGroupIndex = adGroupIndex;
        this.currentAdIndexInAdGroup = adIndexInAdGroup;
        return this;
      }

      /**
       * Sets the current content playback position in milliseconds.
       *
       * <p>This position will be converted to an advancing {@link PositionSupplier} if the overall
       * state indicates an advancing playback position.
       *
       * <p>This method overrides any other {@link PositionSupplier} set via {@link
       * #setContentPositionMs(PositionSupplier)}.
       *
       * @param positionMs The current content playback position in milliseconds, or {@link
       *     C#TIME_UNSET} to indicate the default start position.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setContentPositionMs(long positionMs) {
        this.contentPositionMs = positionMs;
        return this;
      }

      /**
       * Sets the {@link PositionSupplier} for the current content playback position in
       * milliseconds.
       *
       * <p>The supplier is expected to return the updated position on every call if the playback is
       * advancing, for example by using {@link PositionSupplier#getExtrapolating}.
       *
       * <p>This method overrides any other position set via {@link #setContentPositionMs(long)}.
       *
       * @param contentPositionMsSupplier The {@link PositionSupplier} for the current content
       *     playback position in milliseconds, or {@link C#TIME_UNSET} to indicate the default
       *     start position.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setContentPositionMs(PositionSupplier contentPositionMsSupplier) {
        this.contentPositionMs = null;
        this.contentPositionMsSupplier = contentPositionMsSupplier;
        return this;
      }

      /**
       * Sets the current ad playback position in milliseconds. The value is unused if no ad is
       * playing.
       *
       * <p>This position will be converted to an advancing {@link PositionSupplier} if the overall
       * state indicates an advancing ad playback position.
       *
       * <p>This method overrides any other {@link PositionSupplier} set via {@link
       * #setAdPositionMs(PositionSupplier)}.
       *
       * @param positionMs The current ad playback position in milliseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAdPositionMs(long positionMs) {
        this.adPositionMs = positionMs;
        return this;
      }

      /**
       * Sets the {@link PositionSupplier} for the current ad playback position in milliseconds. The
       * value is unused if no ad is playing.
       *
       * <p>The supplier is expected to return the updated position on every call if the playback is
       * advancing, for example by using {@link PositionSupplier#getExtrapolating}.
       *
       * <p>This method overrides any other position set via {@link #setAdPositionMs(long)}.
       *
       * @param adPositionMsSupplier The {@link PositionSupplier} for the current ad playback
       *     position in milliseconds. The value is unused if no ad is playing.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAdPositionMs(PositionSupplier adPositionMsSupplier) {
        this.adPositionMs = null;
        this.adPositionMsSupplier = adPositionMsSupplier;
        return this;
      }

      /**
       * Sets the {@link PositionSupplier} for the estimated position up to which the currently
       * playing content is buffered, in milliseconds.
       *
       * @param contentBufferedPositionMsSupplier The {@link PositionSupplier} for the estimated
       *     position up to which the currently playing content is buffered, in milliseconds, or
       *     {@link C#TIME_UNSET} to indicate the default start position.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setContentBufferedPositionMs(
          PositionSupplier contentBufferedPositionMsSupplier) {
        this.contentBufferedPositionMsSupplier = contentBufferedPositionMsSupplier;
        return this;
      }

      /**
       * Sets the {@link PositionSupplier} for the estimated position up to which the currently
       * playing ad is buffered, in milliseconds. The value is unused if no ad is playing.
       *
       * @param adBufferedPositionMsSupplier The {@link PositionSupplier} for the estimated position
       *     up to which the currently playing ad is buffered, in milliseconds. The value is unused
       *     if no ad is playing.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAdBufferedPositionMs(PositionSupplier adBufferedPositionMsSupplier) {
        this.adBufferedPositionMsSupplier = adBufferedPositionMsSupplier;
        return this;
      }

      /**
       * Sets the {@link PositionSupplier} for the estimated total buffered duration in
       * milliseconds.
       *
       * @param totalBufferedDurationMsSupplier The {@link PositionSupplier} for the estimated total
       *     buffered duration in milliseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setTotalBufferedDurationMs(PositionSupplier totalBufferedDurationMsSupplier) {
        this.totalBufferedDurationMsSupplier = totalBufferedDurationMsSupplier;
        return this;
      }

      /**
       * Signals that a position discontinuity happened since the last player update and sets the
       * reason for it.
       *
       * @param positionDiscontinuityReason The {@linkplain Player.DiscontinuityReason reason} for
       *     the discontinuity.
       * @param discontinuityPositionMs The position, in milliseconds, in the current content or ad
       *     from which playback continues after the discontinuity.
       * @return This builder.
       * @see #clearPositionDiscontinuity
       */
      @CanIgnoreReturnValue
      public Builder setPositionDiscontinuity(
          @Player.DiscontinuityReason int positionDiscontinuityReason,
          long discontinuityPositionMs) {
        this.hasPositionDiscontinuity = true;
        this.positionDiscontinuityReason = positionDiscontinuityReason;
        this.discontinuityPositionMs = discontinuityPositionMs;
        return this;
      }

      /**
       * Clears a previously set position discontinuity signal.
       *
       * @return This builder.
       * @see #hasPositionDiscontinuity
       */
      @CanIgnoreReturnValue
      public Builder clearPositionDiscontinuity() {
        this.hasPositionDiscontinuity = false;
        return this;
      }

      /** Builds the {@link State}. */
      public State build() {
        return new State(this);
      }
    }

    /** The available {@link Commands}. */
    public final Commands availableCommands;

    /** Whether playback should proceed when ready and not suppressed. */
    public final boolean playWhenReady;

    /** The last reason for changing {@link #playWhenReady}. */
    public final @PlayWhenReadyChangeReason int playWhenReadyChangeReason;

    /** The {@linkplain Player.State state} of the player. */
    public final @Player.State int playbackState;

    /** The reason why playback is suppressed even if {@link #getPlayWhenReady()} is true. */
    public final @PlaybackSuppressionReason int playbackSuppressionReason;

    /** The last error that caused playback to fail, or null if there was no error. */
    @Nullable public final PlaybackException playerError;

    /** The {@link RepeatMode} used for playback. */
    public final @RepeatMode int repeatMode;

    /** Whether shuffling of media items is enabled. */
    public final boolean shuffleModeEnabled;

    /** Whether the player is currently loading its source. */
    public final boolean isLoading;

    /** The {@link Player#seekBack()} increment in milliseconds. */
    public final long seekBackIncrementMs;

    /** The {@link Player#seekForward()} increment in milliseconds. */
    public final long seekForwardIncrementMs;

    /**
     * The maximum position for which {@link #seekToPrevious()} seeks to the previous item, in
     * milliseconds.
     */
    public final long maxSeekToPreviousPositionMs;

    /** The currently active {@link PlaybackParameters}. */
    public final PlaybackParameters playbackParameters;

    /** The currently active {@link TrackSelectionParameters}. */
    public final TrackSelectionParameters trackSelectionParameters;

    /** The current {@link AudioAttributes}. */
    public final AudioAttributes audioAttributes;

    /** The current audio volume, with 0 being silence and 1 being unity gain (signal unchanged). */
    @FloatRange(from = 0, to = 1.0)
    public final float volume;

    /** The current video size. */
    public final VideoSize videoSize;

    /** The current {@linkplain CueGroup cues}. */
    public final CueGroup currentCues;

    /** The {@link DeviceInfo}. */
    public final DeviceInfo deviceInfo;

    /** The current device volume. */
    @IntRange(from = 0)
    public final int deviceVolume;

    /** Whether the device is muted. */
    public final boolean isDeviceMuted;

    /** The size of the surface onto which the video is being rendered. */
    public final Size surfaceSize;

    /**
     * Whether a frame has been rendered for the first time since setting the surface, a rendering
     * reset, or since the stream being rendered was changed.
     */
    public final boolean newlyRenderedFirstFrame;

    /** The most recent timed metadata. */
    public final Metadata timedMetadata;

    /** The {@link Timeline}. */
    public final Timeline timeline;

    /** The current {@link Tracks}. */
    public final Tracks currentTracks;

    /** The current combined {@link MediaMetadata}. */
    public final MediaMetadata currentMetadata;

    /** The playlist {@link MediaMetadata}. */
    public final MediaMetadata playlistMetadata;

    /**
     * The current media item index, or {@link C#INDEX_UNSET} to assume the default first item of
     * the playlist is played.
     */
    public final int currentMediaItemIndex;

    /** The current ad group index, or {@link C#INDEX_UNSET} if no ad is playing. */
    public final int currentAdGroupIndex;

    /** The current ad index in the ad group, or {@link C#INDEX_UNSET} if no ad is playing. */
    public final int currentAdIndexInAdGroup;

    /**
     * The {@link PositionSupplier} for the current content playback position in milliseconds, or
     * {@link C#TIME_UNSET} to indicate the default start position.
     */
    public final PositionSupplier contentPositionMsSupplier;

    /**
     * The {@link PositionSupplier} for the current ad playback position in milliseconds. The value
     * is unused if no ad is playing.
     */
    public final PositionSupplier adPositionMsSupplier;

    /**
     * The {@link PositionSupplier} for the estimated position up to which the currently playing
     * content is buffered, in milliseconds, or {@link C#TIME_UNSET} to indicate the default start
     * position.
     */
    public final PositionSupplier contentBufferedPositionMsSupplier;

    /**
     * The {@link PositionSupplier} for the estimated position up to which the currently playing ad
     * is buffered, in milliseconds. The value is unused if no ad is playing.
     */
    public final PositionSupplier adBufferedPositionMsSupplier;

    /** The {@link PositionSupplier} for the estimated total buffered duration in milliseconds. */
    public final PositionSupplier totalBufferedDurationMsSupplier;

    /** Signals that a position discontinuity happened since the last update to the player. */
    public final boolean hasPositionDiscontinuity;

    /**
     * The {@linkplain Player.DiscontinuityReason reason} for the last position discontinuity. The
     * value is unused if {@link #hasPositionDiscontinuity} is {@code false}.
     */
    public final @Player.DiscontinuityReason int positionDiscontinuityReason;

    /**
     * The position, in milliseconds, in the current content or ad from which playback continued
     * after the discontinuity. The value is unused if {@link #hasPositionDiscontinuity} is {@code
     * false}.
     */
    public final long discontinuityPositionMs;

    private State(Builder builder) {
      Tracks currentTracks = builder.currentTracks;
      MediaMetadata currentMetadata = builder.currentMetadata;
      if (builder.timeline.isEmpty()) {
        checkArgument(
            builder.playbackState == Player.STATE_IDLE
                || builder.playbackState == Player.STATE_ENDED,
            "Empty playlist only allowed in STATE_IDLE or STATE_ENDED");
        checkArgument(
            builder.currentAdGroupIndex == C.INDEX_UNSET
                && builder.currentAdIndexInAdGroup == C.INDEX_UNSET,
            "Ads not allowed if playlist is empty");
        if (currentTracks == null) {
          currentTracks = Tracks.EMPTY;
        }
        if (currentMetadata == null) {
          currentMetadata = MediaMetadata.EMPTY;
        }
      } else {
        int mediaItemIndex = builder.currentMediaItemIndex;
        if (mediaItemIndex == C.INDEX_UNSET) {
          mediaItemIndex = 0; // TODO: Use shuffle order to find first index.
        } else {
          checkArgument(
              builder.currentMediaItemIndex < builder.timeline.getWindowCount(),
              "currentMediaItemIndex must be less than playlist.size()");
        }
        if (builder.currentAdGroupIndex != C.INDEX_UNSET) {
          Timeline.Period period = new Timeline.Period();
          Timeline.Window window = new Timeline.Window();
          long contentPositionMs =
              builder.contentPositionMs != null
                  ? builder.contentPositionMs
                  : builder.contentPositionMsSupplier.get();
          int periodIndex =
              getPeriodIndexFromWindowPosition(
                  builder.timeline, mediaItemIndex, contentPositionMs, window, period);
          builder.timeline.getPeriod(periodIndex, period);
          checkArgument(
              builder.currentAdGroupIndex < period.getAdGroupCount(),
              "PeriodData has less ad groups than adGroupIndex");
          int adCountInGroup = period.getAdCountInAdGroup(builder.currentAdGroupIndex);
          if (adCountInGroup != C.LENGTH_UNSET) {
            checkArgument(
                builder.currentAdIndexInAdGroup < adCountInGroup,
                "Ad group has less ads than adIndexInGroupIndex");
          }
        }
        if (builder.playlist != null) {
          MediaItemData mediaItemData = builder.playlist.get(mediaItemIndex);
          currentTracks = mediaItemData.tracks;
          currentMetadata = mediaItemData.mediaMetadata;
        }
        if (currentMetadata == null) {
          currentMetadata =
              getCombinedMediaMetadata(
                  builder.timeline.getWindow(mediaItemIndex, new Timeline.Window()).mediaItem,
                  checkNotNull(currentTracks));
        }
      }
      if (builder.playerError != null) {
        checkArgument(
            builder.playbackState == Player.STATE_IDLE, "Player error only allowed in STATE_IDLE");
      }
      if (builder.playbackState == Player.STATE_IDLE
          || builder.playbackState == Player.STATE_ENDED) {
        checkArgument(
            !builder.isLoading, "isLoading only allowed when not in STATE_IDLE or STATE_ENDED");
      }
      PositionSupplier contentPositionMsSupplier = builder.contentPositionMsSupplier;
      if (builder.contentPositionMs != null) {
        if (builder.currentAdGroupIndex == C.INDEX_UNSET
            && builder.playWhenReady
            && builder.playbackState == Player.STATE_READY
            && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE
            && builder.contentPositionMs != C.TIME_UNSET) {
          contentPositionMsSupplier =
              PositionSupplier.getExtrapolating(
                  builder.contentPositionMs, builder.playbackParameters.speed);
        } else {
          contentPositionMsSupplier = PositionSupplier.getConstant(builder.contentPositionMs);
        }
      }
      PositionSupplier adPositionMsSupplier = builder.adPositionMsSupplier;
      if (builder.adPositionMs != null) {
        if (builder.currentAdGroupIndex != C.INDEX_UNSET
            && builder.playWhenReady
            && builder.playbackState == Player.STATE_READY
            && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) {
          adPositionMsSupplier =
              PositionSupplier.getExtrapolating(builder.adPositionMs, /* playbackSpeed= */ 1f);
        } else {
          adPositionMsSupplier = PositionSupplier.getConstant(builder.adPositionMs);
        }
      }
      this.availableCommands = builder.availableCommands;
      this.playWhenReady = builder.playWhenReady;
      this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason;
      this.playbackState = builder.playbackState;
      this.playbackSuppressionReason = builder.playbackSuppressionReason;
      this.playerError = builder.playerError;
      this.repeatMode = builder.repeatMode;
      this.shuffleModeEnabled = builder.shuffleModeEnabled;
      this.isLoading = builder.isLoading;
      this.seekBackIncrementMs = builder.seekBackIncrementMs;
      this.seekForwardIncrementMs = builder.seekForwardIncrementMs;
      this.maxSeekToPreviousPositionMs = builder.maxSeekToPreviousPositionMs;
      this.playbackParameters = builder.playbackParameters;
      this.trackSelectionParameters = builder.trackSelectionParameters;
      this.audioAttributes = builder.audioAttributes;
      this.volume = builder.volume;
      this.videoSize = builder.videoSize;
      this.currentCues = builder.currentCues;
      this.deviceInfo = builder.deviceInfo;
      this.deviceVolume = builder.deviceVolume;
      this.isDeviceMuted = builder.isDeviceMuted;
      this.surfaceSize = builder.surfaceSize;
      this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame;
      this.timedMetadata = builder.timedMetadata;
      this.timeline = builder.timeline;
      this.currentTracks = checkNotNull(currentTracks);
      this.currentMetadata = currentMetadata;
      this.playlistMetadata = builder.playlistMetadata;
      this.currentMediaItemIndex = builder.currentMediaItemIndex;
      this.currentAdGroupIndex = builder.currentAdGroupIndex;
      this.currentAdIndexInAdGroup = builder.currentAdIndexInAdGroup;
      this.contentPositionMsSupplier = contentPositionMsSupplier;
      this.adPositionMsSupplier = adPositionMsSupplier;
      this.contentBufferedPositionMsSupplier = builder.contentBufferedPositionMsSupplier;
      this.adBufferedPositionMsSupplier = builder.adBufferedPositionMsSupplier;
      this.totalBufferedDurationMsSupplier = builder.totalBufferedDurationMsSupplier;
      this.hasPositionDiscontinuity = builder.hasPositionDiscontinuity;
      this.positionDiscontinuityReason = builder.positionDiscontinuityReason;
      this.discontinuityPositionMs = builder.discontinuityPositionMs;
    }

    /** Returns a {@link Builder} pre-populated with the current state values. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    /**
     * Returns the list of {@link MediaItemData} for the current playlist.
     *
     * @see Builder#setPlaylist(List)
     */
    public ImmutableList<MediaItemData> getPlaylist() {
      if (timeline instanceof PlaylistTimeline) {
        return ((PlaylistTimeline) timeline).playlist;
      }
      Timeline.Window window = new Timeline.Window();
      Timeline.Period period = new Timeline.Period();
      ImmutableList.Builder<MediaItemData> items =
          ImmutableList.builderWithExpectedSize(timeline.getWindowCount());
      for (int i = 0; i < timeline.getWindowCount(); i++) {
        items.add(
            MediaItemData.buildFromState(
                /* state= */ this, /* mediaItemIndex= */ i, period, window));
      }
      return items.build();
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof State)) {
        return false;
      }
      State state = (State) o;
      return playWhenReady == state.playWhenReady
          && playWhenReadyChangeReason == state.playWhenReadyChangeReason
          && availableCommands.equals(state.availableCommands)
          && playbackState == state.playbackState
          && playbackSuppressionReason == state.playbackSuppressionReason
          && Objects.equals(playerError, state.playerError)
          && repeatMode == state.repeatMode
          && shuffleModeEnabled == state.shuffleModeEnabled
          && isLoading == state.isLoading
          && seekBackIncrementMs == state.seekBackIncrementMs
          && seekForwardIncrementMs == state.seekForwardIncrementMs
          && maxSeekToPreviousPositionMs == state.maxSeekToPreviousPositionMs
          && playbackParameters.equals(state.playbackParameters)
          && trackSelectionParameters.equals(state.trackSelectionParameters)
          && audioAttributes.equals(state.audioAttributes)
          && volume == state.volume
          && videoSize.equals(state.videoSize)
          && currentCues.equals(state.currentCues)
          && deviceInfo.equals(state.deviceInfo)
          && deviceVolume == state.deviceVolume
          && isDeviceMuted == state.isDeviceMuted
          && surfaceSize.equals(state.surfaceSize)
          && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame
          && timedMetadata.equals(state.timedMetadata)
          && timeline.equals(state.timeline)
          && currentTracks.equals(state.currentTracks)
          && currentMetadata.equals(state.currentMetadata)
          && playlistMetadata.equals(state.playlistMetadata)
          && currentMediaItemIndex == state.currentMediaItemIndex
          && currentAdGroupIndex == state.currentAdGroupIndex
          && currentAdIndexInAdGroup == state.currentAdIndexInAdGroup
          && contentPositionMsSupplier.equals(state.contentPositionMsSupplier)
          && adPositionMsSupplier.equals(state.adPositionMsSupplier)
          && contentBufferedPositionMsSupplier.equals(state.contentBufferedPositionMsSupplier)
          && adBufferedPositionMsSupplier.equals(state.adBufferedPositionMsSupplier)
          && totalBufferedDurationMsSupplier.equals(state.totalBufferedDurationMsSupplier)
          && hasPositionDiscontinuity == state.hasPositionDiscontinuity
          && positionDiscontinuityReason == state.positionDiscontinuityReason
          && discontinuityPositionMs == state.discontinuityPositionMs;
    }

    @Override
    public int hashCode() {
      int result = 7;
      result = 31 * result + availableCommands.hashCode();
      result = 31 * result + (playWhenReady ? 1 : 0);
      result = 31 * result + playWhenReadyChangeReason;
      result = 31 * result + playbackState;
      result = 31 * result + playbackSuppressionReason;
      result = 31 * result + (playerError == null ? 0 : playerError.hashCode());
      result = 31 * result + repeatMode;
      result = 31 * result + (shuffleModeEnabled ? 1 : 0);
      result = 31 * result + (isLoading ? 1 : 0);
      result = 31 * result + (int) (seekBackIncrementMs ^ (seekBackIncrementMs >>> 32));
      result = 31 * result + (int) (seekForwardIncrementMs ^ (seekForwardIncrementMs >>> 32));
      result =
          31 * result + (int) (maxSeekToPreviousPositionMs ^ (maxSeekToPreviousPositionMs >>> 32));
      result = 31 * result + playbackParameters.hashCode();
      result = 31 * result + trackSelectionParameters.hashCode();
      result = 31 * result + audioAttributes.hashCode();
      result = 31 * result + Float.floatToRawIntBits(volume);
      result = 31 * result + videoSize.hashCode();
      result = 31 * result + currentCues.hashCode();
      result = 31 * result + deviceInfo.hashCode();
      result = 31 * result + deviceVolume;
      result = 31 * result + (isDeviceMuted ? 1 : 0);
      result = 31 * result + surfaceSize.hashCode();
      result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0);
      result = 31 * result + timedMetadata.hashCode();
      result = 31 * result + timeline.hashCode();
      result = 31 * result + currentTracks.hashCode();
      result = 31 * result + currentMetadata.hashCode();
      result = 31 * result + playlistMetadata.hashCode();
      result = 31 * result + currentMediaItemIndex;
      result = 31 * result + currentAdGroupIndex;
      result = 31 * result + currentAdIndexInAdGroup;
      result = 31 * result + contentPositionMsSupplier.hashCode();
      result = 31 * result + adPositionMsSupplier.hashCode();
      result = 31 * result + contentBufferedPositionMsSupplier.hashCode();
      result = 31 * result + adBufferedPositionMsSupplier.hashCode();
      result = 31 * result + totalBufferedDurationMsSupplier.hashCode();
      result = 31 * result + (hasPositionDiscontinuity ? 1 : 0);
      result = 31 * result + positionDiscontinuityReason;
      result = 31 * result + (int) (discontinuityPositionMs ^ (discontinuityPositionMs >>> 32));
      return result;
    }
  }

  private static final class PlaylistTimeline extends Timeline {

    private final ImmutableList<MediaItemData> playlist;
    private final int[] firstPeriodIndexByWindowIndex;
    private final int[] windowIndexByPeriodIndex;
    private final HashMap<Object, Integer> periodIndexByUid;

    public PlaylistTimeline(List<MediaItemData> playlist) {
      int mediaItemCount = playlist.size();
      this.playlist = ImmutableList.copyOf(playlist);
      this.firstPeriodIndexByWindowIndex = new int[mediaItemCount];
      int periodCount = 0;
      for (int i = 0; i < mediaItemCount; i++) {
        MediaItemData mediaItemData = playlist.get(i);
        firstPeriodIndexByWindowIndex[i] = periodCount;
        periodCount += getPeriodCountInMediaItem(mediaItemData);
      }
      this.windowIndexByPeriodIndex = new int[periodCount];
      this.periodIndexByUid = new HashMap<>();
      int periodIndex = 0;
      for (int i = 0; i < mediaItemCount; i++) {
        MediaItemData mediaItemData = playlist.get(i);
        for (int j = 0; j < getPeriodCountInMediaItem(mediaItemData); j++) {
          periodIndexByUid.put(mediaItemData.getPeriodUid(j), periodIndex);
          windowIndexByPeriodIndex[periodIndex] = i;
          periodIndex++;
        }
      }
    }

    @Override
    public int getWindowCount() {
      return playlist.size();
    }

    @Override
    public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
      // TODO: Support shuffle order.
      return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
    }

    @Override
    public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
      // TODO: Support shuffle order.
      return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled);
    }

    @Override
    public int getLastWindowIndex(boolean shuffleModeEnabled) {
      // TODO: Support shuffle order.
      return super.getLastWindowIndex(shuffleModeEnabled);
    }

    @Override
    public int getFirstWindowIndex(boolean shuffleModeEnabled) {
      // TODO: Support shuffle order.
      return super.getFirstWindowIndex(shuffleModeEnabled);
    }

    @Override
    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
      return playlist
          .get(windowIndex)
          .getWindow(firstPeriodIndexByWindowIndex[windowIndex], window);
    }

    @Override
    public int getPeriodCount() {
      return windowIndexByPeriodIndex.length;
    }

    @Override
    public Period getPeriodByUid(Object periodUid, Period period) {
      int periodIndex = checkNotNull(periodIndexByUid.get(periodUid));
      return getPeriod(periodIndex, period, /* setIds= */ true);
    }

    @Override
    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
      int windowIndex = windowIndexByPeriodIndex[periodIndex];
      int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex];
      return playlist.get(windowIndex).getPeriod(windowIndex, periodIndexInWindow, period);
    }

    @Override
    public int getIndexOfPeriod(Object uid) {
      @Nullable Integer index = periodIndexByUid.get(uid);
      return index == null ? C.INDEX_UNSET : index;
    }

    @Override
    public Object getUidOfPeriod(int periodIndex) {
      int windowIndex = windowIndexByPeriodIndex[periodIndex];
      int periodIndexInWindow = periodIndex - firstPeriodIndexByWindowIndex[windowIndex];
      return playlist.get(windowIndex).getPeriodUid(periodIndexInWindow);
    }

    private static int getPeriodCountInMediaItem(MediaItemData mediaItemData) {
      return mediaItemData.periods.isEmpty() ? 1 : mediaItemData.periods.size();
    }
  }

  /**
   * An immutable description of an item in the playlist, containing both static setup information
   * like {@link MediaItem} and dynamic data that is generally read from the media like the
   * duration.
   */
  protected static final class MediaItemData {

    /** A builder for {@link MediaItemData} objects. */
    public static final class Builder {

      private Object uid;
      private Tracks tracks;
      private MediaItem mediaItem;
      @Nullable private MediaMetadata mediaMetadata;
      @Nullable private Object manifest;
      @Nullable private MediaItem.LiveConfiguration liveConfiguration;
      private long presentationStartTimeMs;
      private long windowStartTimeMs;
      private long elapsedRealtimeEpochOffsetMs;
      private boolean isSeekable;
      private boolean isDynamic;
      private long defaultPositionUs;
      private long durationUs;
      private long positionInFirstPeriodUs;
      private boolean isPlaceholder;
      private ImmutableList<PeriodData> periods;

      /**
       * Creates the builder.
       *
       * @param uid The unique identifier of the media item within a playlist. This value will be
       *     set as {@link Timeline.Window#uid} for this item.
       */
      public Builder(Object uid) {
        this.uid = uid;
        tracks = Tracks.EMPTY;
        mediaItem = MediaItem.EMPTY;
        mediaMetadata = null;
        manifest = null;
        liveConfiguration = null;
        presentationStartTimeMs = C.TIME_UNSET;
        windowStartTimeMs = C.TIME_UNSET;
        elapsedRealtimeEpochOffsetMs = C.TIME_UNSET;
        isSeekable = false;
        isDynamic = false;
        defaultPositionUs = 0;
        durationUs = C.TIME_UNSET;
        positionInFirstPeriodUs = 0;
        isPlaceholder = false;
        periods = ImmutableList.of();
      }

      private Builder(MediaItemData mediaItemData) {
        this.uid = mediaItemData.uid;
        this.tracks = mediaItemData.tracks;
        this.mediaItem = mediaItemData.mediaItem;
        this.mediaMetadata = mediaItemData.mediaMetadata;
        this.manifest = mediaItemData.manifest;
        this.liveConfiguration = mediaItemData.liveConfiguration;
        this.presentationStartTimeMs = mediaItemData.presentationStartTimeMs;
        this.windowStartTimeMs = mediaItemData.windowStartTimeMs;
        this.elapsedRealtimeEpochOffsetMs = mediaItemData.elapsedRealtimeEpochOffsetMs;
        this.isSeekable = mediaItemData.isSeekable;
        this.isDynamic = mediaItemData.isDynamic;
        this.defaultPositionUs = mediaItemData.defaultPositionUs;
        this.durationUs = mediaItemData.durationUs;
        this.positionInFirstPeriodUs = mediaItemData.positionInFirstPeriodUs;
        this.isPlaceholder = mediaItemData.isPlaceholder;
        this.periods = mediaItemData.periods;
      }

      /**
       * Sets the unique identifier of this media item within a playlist.
       *
       * <p>This value will be set as {@link Timeline.Window#uid} for this item.
       *
       * @param uid The unique identifier of this media item within a playlist.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setUid(Object uid) {
        this.uid = uid;
        return this;
      }

      /**
       * Sets the {@link Tracks} of this media item.
       *
       * @param tracks The {@link Tracks} of this media item.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setTracks(Tracks tracks) {
        this.tracks = tracks;
        return this;
      }

      /**
       * Sets the {@link MediaItem}.
       *
       * @param mediaItem The {@link MediaItem}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setMediaItem(MediaItem mediaItem) {
        this.mediaItem = mediaItem;
        return this;
      }

      /**
       * Sets the {@link MediaMetadata}.
       *
       * <p>This data includes static data from the {@link MediaItem#mediaMetadata MediaItem} and
       * the media's {@link Format#metadata Format}, as well any dynamic metadata that has been
       * parsed from the media. If null, the metadata is assumed to be the simple combination of the
       * {@link MediaItem#mediaMetadata MediaItem} metadata and the metadata of the selected {@link
       * Format#metadata Formats}.
       *
       * @param mediaMetadata The {@link MediaMetadata}, or null to assume that the metadata is the
       *     simple combination of the {@link MediaItem#mediaMetadata MediaItem} metadata and the
       *     metadata of the selected {@link Format#metadata Formats}.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setMediaMetadata(@Nullable MediaMetadata mediaMetadata) {
        this.mediaMetadata = mediaMetadata;
        return this;
      }

      /**
       * Sets the manifest of the media item.
       *
       * @param manifest The manifest of the media item, or null if not applicable.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setManifest(@Nullable Object manifest) {
        this.manifest = manifest;
        return this;
      }

      /**
       * Sets the active {@link MediaItem.LiveConfiguration}, or null if the media item is not live.
       *
       * @param liveConfiguration The active {@link MediaItem.LiveConfiguration}, or null if the
       *     media item is not live.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setLiveConfiguration(@Nullable MediaItem.LiveConfiguration liveConfiguration) {
        this.liveConfiguration = liveConfiguration;
        return this;
      }

      /**
       * Sets the start time of the live presentation.
       *
       * <p>This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is
       * {@linkplain #setLiveConfiguration live}.
       *
       * @param presentationStartTimeMs The start time of the live presentation, in milliseconds
       *     since the Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPresentationStartTimeMs(long presentationStartTimeMs) {
        this.presentationStartTimeMs = presentationStartTimeMs;
        return this;
      }

      /**
       * Sets the start time of the live window.
       *
       * <p>This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is
       * {@linkplain #setLiveConfiguration live}. The value should also be greater or equal than the
       * {@linkplain #setPresentationStartTimeMs presentation start time}, if set.
       *
       * @param windowStartTimeMs The start time of the live window, in milliseconds since the Unix
       *     epoch, or {@link C#TIME_UNSET} if unknown or not applicable.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setWindowStartTimeMs(long windowStartTimeMs) {
        this.windowStartTimeMs = windowStartTimeMs;
        return this;
      }

      /**
       * Sets the offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix
       * epoch according to the clock of the media origin server.
       *
       * <p>This value can only be set to anything other than {@link C#TIME_UNSET} if the stream is
       * {@linkplain #setLiveConfiguration live}.
       *
       * @param elapsedRealtimeEpochOffsetMs The offset between {@link
       *     SystemClock#elapsedRealtime()} and the time since the Unix epoch according to the clock
       *     of the media origin server, or {@link C#TIME_UNSET} if unknown or not applicable.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setElapsedRealtimeEpochOffsetMs(long elapsedRealtimeEpochOffsetMs) {
        this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs;
        return this;
      }

      /**
       * Sets whether it's possible to seek within this media item.
       *
       * @param isSeekable Whether it's possible to seek within this media item.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsSeekable(boolean isSeekable) {
        this.isSeekable = isSeekable;
        return this;
      }

      /**
       * Sets whether this media item may change over time, for example a moving live window.
       *
       * @param isDynamic Whether this media item may change over time, for example a moving live
       *     window.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsDynamic(boolean isDynamic) {
        this.isDynamic = isDynamic;
        return this;
      }

      /**
       * Sets the default position relative to the start of the media item at which to begin
       * playback, in microseconds.
       *
       * <p>The default position must be less or equal to the {@linkplain #setDurationUs duration},
       * if set.
       *
       * @param defaultPositionUs The default position relative to the start of the media item at
       *     which to begin playback, in microseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setDefaultPositionUs(long defaultPositionUs) {
        checkArgument(defaultPositionUs >= 0);
        this.defaultPositionUs = defaultPositionUs;
        return this;
      }

      /**
       * Sets the duration of the media item, in microseconds.
       *
       * <p>If both this duration and all {@linkplain #setPeriods period} durations are set, the sum
       * of this duration and the {@linkplain #setPositionInFirstPeriodUs offset in the first
       * period} must match the total duration of all periods.
       *
       * @param durationUs The duration of the media item, in microseconds, or {@link C#TIME_UNSET}
       *     if unknown.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setDurationUs(long durationUs) {
        checkArgument(durationUs == C.TIME_UNSET || durationUs >= 0);
        this.durationUs = durationUs;
        return this;
      }

      /**
       * Sets the position of the start of this media item relative to the start of the first period
       * belonging to it, in microseconds.
       *
       * @param positionInFirstPeriodUs The position of the start of this media item relative to the
       *     start of the first period belonging to it, in microseconds.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPositionInFirstPeriodUs(long positionInFirstPeriodUs) {
        checkArgument(positionInFirstPeriodUs >= 0);
        this.positionInFirstPeriodUs = positionInFirstPeriodUs;
        return this;
      }

      /**
       * Sets whether this media item contains placeholder information because the real information
       * has yet to be loaded.
       *
       * @param isPlaceholder Whether this media item contains placeholder information because the
       *     real information has yet to be loaded.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsPlaceholder(boolean isPlaceholder) {
        this.isPlaceholder = isPlaceholder;
        return this;
      }

      /**
       * Sets the list of {@linkplain PeriodData periods} in this media item.
       *
       * <p>All periods must have unique {@linkplain PeriodData.Builder#setUid UIDs} and only the
       * last period is allowed to have an unset {@linkplain PeriodData.Builder#setDurationUs
       * duration}.
       *
       * @param periods The list of {@linkplain PeriodData periods} in this media item, or an empty
       *     list to assume a single period without ads and the same duration as the media item.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setPeriods(List<PeriodData> periods) {
        int periodCount = periods.size();
        for (int i = 0; i < periodCount - 1; i++) {
          checkArgument(
              periods.get(i).durationUs != C.TIME_UNSET, "Periods other than last need a duration");
          for (int j = i + 1; j < periodCount; j++) {
            checkArgument(
                !periods.get(i).uid.equals(periods.get(j).uid),
                "Duplicate PeriodData UIDs in period list");
          }
        }
        this.periods = ImmutableList.copyOf(periods);
        return this;
      }

      /** Builds the {@link MediaItemData}. */
      public MediaItemData build() {
        return new MediaItemData(this);
      }
    }

    /** The unique identifier of this media item. */
    public final Object uid;

    /** The {@link Tracks} of this media item. */
    public final Tracks tracks;

    /** The {@link MediaItem}. */
    public final MediaItem mediaItem;

    /**
     * The {@link MediaMetadata}, including static data from the {@link MediaItem#mediaMetadata
     * MediaItem} and the media's {@link Format#metadata Format}, as well any dynamic metadata that
     * has been parsed from the media. If null, the metadata is assumed to be the simple combination
     * of the {@link MediaItem#mediaMetadata MediaItem} metadata and the metadata of the selected
     * {@link Format#metadata Formats}.
     */
    @Nullable public final MediaMetadata mediaMetadata;

    /** The manifest of the media item, or null if not applicable. */
    @Nullable public final Object manifest;

    /** The active {@link MediaItem.LiveConfiguration}, or null if the media item is not live. */
    @Nullable public final MediaItem.LiveConfiguration liveConfiguration;

    /**
     * The start time of the live presentation, in milliseconds since the Unix epoch, or {@link
     * C#TIME_UNSET} if unknown or not applicable.
     */
    public final long presentationStartTimeMs;

    /**
     * The start time of the live window, in milliseconds since the Unix epoch, or {@link
     * C#TIME_UNSET} if unknown or not applicable.
     */
    public final long windowStartTimeMs;

    /**
     * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch
     * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not
     * applicable.
     */
    public final long elapsedRealtimeEpochOffsetMs;

    /** Whether it's possible to seek within this media item. */
    public final boolean isSeekable;

    /** Whether this media item may change over time, for example a moving live window. */
    public final boolean isDynamic;

    /**
     * The default position relative to the start of the media item at which to begin playback, in
     * microseconds.
     */
    public final long defaultPositionUs;

    /** The duration of the media item, in microseconds, or {@link C#TIME_UNSET} if unknown. */
    public final long durationUs;

    /**
     * The position of the start of this media item relative to the start of the first period
     * belonging to it, in microseconds.
     */
    public final long positionInFirstPeriodUs;

    /**
     * Whether this media item contains placeholder information because the real information has yet
     * to be loaded.
     */
    public final boolean isPlaceholder;

    /**
     * The list of {@linkplain PeriodData periods} in this media item, or an empty list to assume a
     * single period without ads and the same duration as the media item.
     */
    public final ImmutableList<PeriodData> periods;

    private final long[] periodPositionInWindowUs;

    private MediaItemData(Builder builder) {
      if (builder.liveConfiguration == null) {
        checkArgument(
            builder.presentationStartTimeMs == C.TIME_UNSET,
            "presentationStartTimeMs can only be set if liveConfiguration != null");
        checkArgument(
            builder.windowStartTimeMs == C.TIME_UNSET,
            "windowStartTimeMs can only be set if liveConfiguration != null");
        checkArgument(
            builder.elapsedRealtimeEpochOffsetMs == C.TIME_UNSET,
            "elapsedRealtimeEpochOffsetMs can only be set if liveConfiguration != null");
      } else if (builder.presentationStartTimeMs != C.TIME_UNSET
          && builder.windowStartTimeMs != C.TIME_UNSET) {
        checkArgument(
            builder.windowStartTimeMs >= builder.presentationStartTimeMs,
            "windowStartTimeMs can't be less than presentationStartTimeMs");
      }
      int periodCount = builder.periods.size();
      if (builder.durationUs != C.TIME_UNSET) {
        checkArgument(
            builder.defaultPositionUs <= builder.durationUs,
            "defaultPositionUs can't be greater than durationUs");
      }
      this.uid = builder.uid;
      this.tracks = builder.tracks;
      this.mediaItem = builder.mediaItem;
      this.mediaMetadata = builder.mediaMetadata;
      this.manifest = builder.manifest;
      this.liveConfiguration = builder.liveConfiguration;
      this.presentationStartTimeMs = builder.presentationStartTimeMs;
      this.windowStartTimeMs = builder.windowStartTimeMs;
      this.elapsedRealtimeEpochOffsetMs = builder.elapsedRealtimeEpochOffsetMs;
      this.isSeekable = builder.isSeekable;
      this.isDynamic = builder.isDynamic;
      this.defaultPositionUs = builder.defaultPositionUs;
      this.durationUs = builder.durationUs;
      this.positionInFirstPeriodUs = builder.positionInFirstPeriodUs;
      this.isPlaceholder = builder.isPlaceholder;
      this.periods = builder.periods;
      periodPositionInWindowUs = new long[periods.size()];
      if (!periods.isEmpty()) {
        periodPositionInWindowUs[0] = -positionInFirstPeriodUs;
        for (int i = 0; i < periodCount - 1; i++) {
          periodPositionInWindowUs[i + 1] = periodPositionInWindowUs[i] + periods.get(i).durationUs;
        }
      }
    }

    /** Returns a {@link Builder} pre-populated with the current values. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof MediaItemData)) {
        return false;
      }
      MediaItemData mediaItemData = (MediaItemData) o;
      return this.uid.equals(mediaItemData.uid)
          && this.tracks.equals(mediaItemData.tracks)
          && this.mediaItem.equals(mediaItemData.mediaItem)
          && Util.areEqual(this.mediaMetadata, mediaItemData.mediaMetadata)
          && Util.areEqual(this.manifest, mediaItemData.manifest)
          && Util.areEqual(this.liveConfiguration, mediaItemData.liveConfiguration)
          && this.presentationStartTimeMs == mediaItemData.presentationStartTimeMs
          && this.windowStartTimeMs == mediaItemData.windowStartTimeMs
          && this.elapsedRealtimeEpochOffsetMs == mediaItemData.elapsedRealtimeEpochOffsetMs
          && this.isSeekable == mediaItemData.isSeekable
          && this.isDynamic == mediaItemData.isDynamic
          && this.defaultPositionUs == mediaItemData.defaultPositionUs
          && this.durationUs == mediaItemData.durationUs
          && this.positionInFirstPeriodUs == mediaItemData.positionInFirstPeriodUs
          && this.isPlaceholder == mediaItemData.isPlaceholder
          && this.periods.equals(mediaItemData.periods);
    }

    @Override
    public int hashCode() {
      int result = 7;
      result = 31 * result + uid.hashCode();
      result = 31 * result + tracks.hashCode();
      result = 31 * result + mediaItem.hashCode();
      result = 31 * result + (mediaMetadata == null ? 0 : mediaMetadata.hashCode());
      result = 31 * result + (manifest == null ? 0 : manifest.hashCode());
      result = 31 * result + (liveConfiguration == null ? 0 : liveConfiguration.hashCode());
      result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32));
      result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32));
      result =
          31 * result
              + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32));
      result = 31 * result + (isSeekable ? 1 : 0);
      result = 31 * result + (isDynamic ? 1 : 0);
      result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32));
      result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
      result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
      result = 31 * result + (isPlaceholder ? 1 : 0);
      result = 31 * result + periods.hashCode();
      return result;
    }

    private static MediaItemData buildFromState(
        State state, int mediaItemIndex, Timeline.Period period, Timeline.Window window) {
      boolean isCurrentItem = getCurrentMediaItemIndexInternal(state) == mediaItemIndex;
      state.timeline.getWindow(mediaItemIndex, window);
      ImmutableList.Builder<PeriodData> periods = ImmutableList.builder();
      for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) {
        state.timeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true);
        periods.add(
            new PeriodData.Builder(checkNotNull(period.uid))
                .setAdPlaybackState(period.adPlaybackState)
                .setDurationUs(period.durationUs)
                .setIsPlaceholder(period.isPlaceholder)
                .build());
      }
      return new MediaItemData.Builder(window.uid)
          .setDefaultPositionUs(window.defaultPositionUs)
          .setDurationUs(window.durationUs)
          .setElapsedRealtimeEpochOffsetMs(window.elapsedRealtimeEpochOffsetMs)
          .setIsDynamic(window.isDynamic)
          .setIsPlaceholder(window.isPlaceholder)
          .setIsSeekable(window.isSeekable)
          .setLiveConfiguration(window.liveConfiguration)
          .setManifest(window.manifest)
          .setMediaItem(window.mediaItem)
          .setMediaMetadata(isCurrentItem ? state.currentMetadata : null)
          .setPeriods(periods.build())
          .setPositionInFirstPeriodUs(window.positionInFirstPeriodUs)
          .setPresentationStartTimeMs(window.presentationStartTimeMs)
          .setTracks(isCurrentItem ? state.currentTracks : Tracks.EMPTY)
          .setWindowStartTimeMs(window.windowStartTimeMs)
          .build();
    }

    private Timeline.Window getWindow(int firstPeriodIndex, Timeline.Window window) {
      int periodCount = periods.isEmpty() ? 1 : periods.size();
      window.set(
          uid,
          mediaItem,
          manifest,
          presentationStartTimeMs,
          windowStartTimeMs,
          elapsedRealtimeEpochOffsetMs,
          isSeekable,
          isDynamic,
          liveConfiguration,
          defaultPositionUs,
          durationUs,
          firstPeriodIndex,
          /* lastPeriodIndex= */ firstPeriodIndex + periodCount - 1,
          positionInFirstPeriodUs);
      window.isPlaceholder = isPlaceholder;
      return window;
    }

    private Timeline.Period getPeriod(
        int windowIndex, int periodIndexInMediaItem, Timeline.Period period) {
      if (periods.isEmpty()) {
        period.set(
            /* id= */ uid,
            uid,
            windowIndex,
            /* durationUs= */ positionInFirstPeriodUs + durationUs,
            /* positionInWindowUs= */ 0,
            AdPlaybackState.NONE,
            isPlaceholder);
      } else {
        PeriodData periodData = periods.get(periodIndexInMediaItem);
        Object periodId = periodData.uid;
        Object periodUid = Pair.create(uid, periodId);
        period.set(
            periodId,
            periodUid,
            windowIndex,
            periodData.durationUs,
            periodPositionInWindowUs[periodIndexInMediaItem],
            periodData.adPlaybackState,
            periodData.isPlaceholder);
      }
      return period;
    }

    private Object getPeriodUid(int periodIndexInMediaItem) {
      if (periods.isEmpty()) {
        return uid;
      }
      Object periodId = periods.get(periodIndexInMediaItem).uid;
      return Pair.create(uid, periodId);
    }
  }

  /** Data describing the properties of a period inside a {@link MediaItemData}. */
  protected static final class PeriodData {

    /** A builder for {@link PeriodData} objects. */
    public static final class Builder {

      private Object uid;
      private long durationUs;
      private AdPlaybackState adPlaybackState;
      private boolean isPlaceholder;

      /**
       * Creates the builder.
       *
       * @param uid The unique identifier of the period within its media item.
       */
      public Builder(Object uid) {
        this.uid = uid;
        this.durationUs = 0;
        this.adPlaybackState = AdPlaybackState.NONE;
        this.isPlaceholder = false;
      }

      private Builder(PeriodData periodData) {
        this.uid = periodData.uid;
        this.durationUs = periodData.durationUs;
        this.adPlaybackState = periodData.adPlaybackState;
        this.isPlaceholder = periodData.isPlaceholder;
      }

      /**
       * Sets the unique identifier of the period within its media item.
       *
       * @param uid The unique identifier of the period within its media item.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setUid(Object uid) {
        this.uid = uid;
        return this;
      }

      /**
       * Sets the total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown.
       *
       * <p>Only the last period in a media item can have an unknown duration.
       *
       * @param durationUs The total duration of the period, in microseconds, or {@link
       *     C#TIME_UNSET} if unknown.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setDurationUs(long durationUs) {
        checkArgument(durationUs == C.TIME_UNSET || durationUs >= 0);
        this.durationUs = durationUs;
        return this;
      }

      /**
       * Sets the {@link AdPlaybackState}.
       *
       * @param adPlaybackState The {@link AdPlaybackState}, or {@link AdPlaybackState#NONE} if
       *     there are no ads.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setAdPlaybackState(AdPlaybackState adPlaybackState) {
        this.adPlaybackState = adPlaybackState;
        return this;
      }

      /**
       * Sets whether this period contains placeholder information because the real information has
       * yet to be loaded
       *
       * @param isPlaceholder Whether this period contains placeholder information because the real
       *     information has yet to be loaded.
       * @return This builder.
       */
      @CanIgnoreReturnValue
      public Builder setIsPlaceholder(boolean isPlaceholder) {
        this.isPlaceholder = isPlaceholder;
        return this;
      }

      /** Builds the {@link PeriodData}. */
      public PeriodData build() {
        return new PeriodData(this);
      }
    }

    /** The unique identifier of the period within its media item. */
    public final Object uid;

    /**
     * The total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. Only
     * the last period in a media item can have an unknown duration.
     */
    public final long durationUs;

    /**
     * The {@link AdPlaybackState} of the period, or {@link AdPlaybackState#NONE} if there are no
     * ads.
     */
    public final AdPlaybackState adPlaybackState;

    /**
     * Whether this period contains placeholder information because the real information has yet to
     * be loaded.
     */
    public final boolean isPlaceholder;

    private PeriodData(Builder builder) {
      this.uid = builder.uid;
      this.durationUs = builder.durationUs;
      this.adPlaybackState = builder.adPlaybackState;
      this.isPlaceholder = builder.isPlaceholder;
    }

    /** Returns a {@link Builder} pre-populated with the current values. */
    public Builder buildUpon() {
      return new Builder(this);
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof PeriodData)) {
        return false;
      }
      PeriodData periodData = (PeriodData) o;
      return this.uid.equals(periodData.uid)
          && this.durationUs == periodData.durationUs
          && this.adPlaybackState.equals(periodData.adPlaybackState)
          && this.isPlaceholder == periodData.isPlaceholder;
    }

    @Override
    public int hashCode() {
      int result = 7;
      result = 31 * result + uid.hashCode();
      result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
      result = 31 * result + adPlaybackState.hashCode();
      result = 31 * result + (isPlaceholder ? 1 : 0);
      return result;
    }
  }

  /** A supplier for a position. */
  protected interface PositionSupplier {

    /** An instance returning a constant position of zero. */
    PositionSupplier ZERO = getConstant(/* positionMs= */ 0);

    /**
     * Returns an instance that returns a constant value.
     *
     * @param positionMs The constant position to return, in milliseconds.
     */
    static PositionSupplier getConstant(long positionMs) {
      return () -> positionMs;
    }

    /**
     * Returns an instance that extrapolates the provided position into the future.
     *
     * @param currentPositionMs The current position in milliseconds.
     * @param playbackSpeed The playback speed with which the position is assumed to increase.
     */
    static PositionSupplier getExtrapolating(long currentPositionMs, float playbackSpeed) {
      long startTimeMs = SystemClock.elapsedRealtime();
      return () -> {
        long currentTimeMs = SystemClock.elapsedRealtime();
        return currentPositionMs + (long) ((currentTimeMs - startTimeMs) * playbackSpeed);
      };
    }

    /** Returns the position. */
    long get();
  }

  /**
   * Position difference threshold below which we do not automatically report a position
   * discontinuity, in milliseconds.
   */
  private static final long POSITION_DISCONTINUITY_THRESHOLD_MS = 1000;

  private final ListenerSet<Listener> listeners;
  private final Looper applicationLooper;
  private final HandlerWrapper applicationHandler;
  private final HashSet<ListenableFuture<?>> pendingOperations;
  private final Timeline.Period period;

  private @MonotonicNonNull State state;
  private boolean released;

  /**
   * Creates the base class.
   *
   * @param applicationLooper The {@link Looper} that must be used for all calls to the player and
   *     that is used to call listeners on.
   */
  protected SimpleBasePlayer(Looper applicationLooper) {
    this(applicationLooper, Clock.DEFAULT);
  }

  /**
   * Creates the base class.
   *
   * @param applicationLooper The {@link Looper} that must be used for all calls to the player and
   *     that is used to call listeners on.
   * @param clock The {@link Clock} that will be used by the player.
   */
  protected SimpleBasePlayer(Looper applicationLooper, Clock clock) {
    this.applicationLooper = applicationLooper;
    applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null);
    pendingOperations = new HashSet<>();
    period = new Timeline.Period();
    @SuppressWarnings("nullness:argument.type.incompatible") // Using this in constructor.
    ListenerSet<Player.Listener> listenerSet =
        new ListenerSet<>(
            applicationLooper,
            clock,
            (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
    listeners = listenerSet;
  }

  @Override
  public final void addListener(Listener listener) {
    // Don't verify application thread. We allow calls to this method from any thread.
    listeners.add(checkNotNull(listener));
  }

  @Override
  public final void removeListener(Listener listener) {
    verifyApplicationThreadAndInitState();
    listeners.remove(listener);
  }

  @Override
  public final Looper getApplicationLooper() {
    // Don't verify application thread. We allow calls to this method from any thread.
    return applicationLooper;
  }

  @Override
  public final Commands getAvailableCommands() {
    verifyApplicationThreadAndInitState();
    return state.availableCommands;
  }

  @Override
  public final void setPlayWhenReady(boolean playWhenReady) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_PLAY_PAUSE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetPlayWhenReady(playWhenReady),
        /* placeholderStateSupplier= */ () ->
            state
                .buildUpon()
                .setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
                .build());
  }

  @Override
  public final boolean getPlayWhenReady() {
    verifyApplicationThreadAndInitState();
    return state.playWhenReady;
  }

  @Override
  public final void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
    verifyApplicationThreadAndInitState();
    int startIndex = resetPosition ? C.INDEX_UNSET : state.currentMediaItemIndex;
    long startPositionMs = resetPosition ? C.TIME_UNSET : state.contentPositionMsSupplier.get();
    setMediaItemsInternal(mediaItems, startIndex, startPositionMs);
  }

  @Override
  public final void setMediaItems(
      List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
    verifyApplicationThreadAndInitState();
    if (startIndex == C.INDEX_UNSET) {
      startIndex = state.currentMediaItemIndex;
      startPositionMs = state.contentPositionMsSupplier.get();
    }
    setMediaItemsInternal(mediaItems, startIndex, startPositionMs);
  }

  @RequiresNonNull("state")
  private void setMediaItemsInternal(
      List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
    checkArgument(startIndex == C.INDEX_UNSET || startIndex >= 0);
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS)
        && (mediaItems.size() != 1 || !shouldHandleCommand(Player.COMMAND_SET_MEDIA_ITEM))) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetMediaItems(mediaItems, startIndex, startPositionMs),
        /* placeholderStateSupplier= */ () -> {
          ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>();
          for (int i = 0; i < mediaItems.size(); i++) {
            placeholderPlaylist.add(getPlaceholderMediaItemData(mediaItems.get(i)));
          }
          return getStateWithNewPlaylistAndPosition(
              state, placeholderPlaylist, startIndex, startPositionMs, window);
        });
  }

  @Override
  public final void addMediaItems(int index, List<MediaItem> mediaItems) {
    verifyApplicationThreadAndInitState();
    checkArgument(index >= 0);
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    int playlistSize = state.timeline.getWindowCount();
    if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || mediaItems.isEmpty()) {
      return;
    }
    int correctedIndex = min(index, playlistSize);
    updateStateForPendingOperation(
        /* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems),
        /* placeholderStateSupplier= */ () -> {
          List<MediaItemData> placeholderPlaylist =
              buildMutablePlaylistFromState(state, period, window);
          for (int i = 0; i < mediaItems.size(); i++) {
            placeholderPlaylist.add(
                i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
          }
          if (!state.timeline.isEmpty()) {
            return getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
          } else {
            // Handle initial position update when these are the first items added to the playlist.
            return getStateWithNewPlaylistAndPosition(
                state,
                placeholderPlaylist,
                state.currentMediaItemIndex,
                state.contentPositionMsSupplier.get(),
                window);
          }
        });
  }

  @Override
  public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
    verifyApplicationThreadAndInitState();
    checkArgument(fromIndex >= 0 && toIndex >= fromIndex && newIndex >= 0);
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    int playlistSize = state.timeline.getWindowCount();
    if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS)
        || playlistSize == 0
        || fromIndex >= playlistSize) {
      return;
    }
    int correctedToIndex = min(toIndex, playlistSize);
    int correctedNewIndex = min(newIndex, playlistSize - (correctedToIndex - fromIndex));
    if (fromIndex == correctedToIndex || correctedNewIndex == fromIndex) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleMoveMediaItems(
            fromIndex, correctedToIndex, correctedNewIndex),
        /* placeholderStateSupplier= */ () -> {
          List<MediaItemData> placeholderPlaylist =
              buildMutablePlaylistFromState(state, period, window);
          Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex);
          return getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
        });
  }

  @Override
  public final void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
    verifyApplicationThreadAndInitState();
    checkArgument(fromIndex >= 0 && fromIndex <= toIndex);
    State state = this.state;
    int playlistSize = state.timeline.getWindowCount();
    if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || fromIndex > playlistSize) {
      return;
    }
    int correctedToIndex = min(toIndex, playlistSize);
    updateStateForPendingOperation(
        /* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems),
        /* placeholderStateSupplier= */ () -> {
          List<MediaItemData> placeholderPlaylist =
              buildMutablePlaylistFromState(state, period, window);
          for (int i = 0; i < mediaItems.size(); i++) {
            placeholderPlaylist.add(
                i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
          }
          State updatedState;
          if (!state.timeline.isEmpty()) {
            updatedState = getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
          } else {
            // Handle initial position update when these are the first items added to the playlist.
            updatedState =
                getStateWithNewPlaylistAndPosition(
                    state,
                    placeholderPlaylist,
                    state.currentMediaItemIndex,
                    state.contentPositionMsSupplier.get(),
                    window);
          }
          if (fromIndex < correctedToIndex) {
            Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
            return getStateWithNewPlaylist(updatedState, placeholderPlaylist, period, window);
          } else {
            return updatedState;
          }
        });
  }

  @Override
  public final void removeMediaItems(int fromIndex, int toIndex) {
    verifyApplicationThreadAndInitState();
    checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    int playlistSize = state.timeline.getWindowCount();
    if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS)
        || playlistSize == 0
        || fromIndex >= playlistSize) {
      return;
    }
    int correctedToIndex = min(toIndex, playlistSize);
    if (fromIndex == correctedToIndex) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex),
        /* placeholderStateSupplier= */ () -> {
          List<MediaItemData> placeholderPlaylist =
              buildMutablePlaylistFromState(state, period, window);
          Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
          return getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
        });
  }

  @Override
  public final void prepare() {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_PREPARE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handlePrepare(),
        /* placeholderStateSupplier= */ () ->
            state
                .buildUpon()
                .setPlayerError(null)
                .setPlaybackState(state.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING)
                .build());
  }

  @Override
  @Player.State
  public final int getPlaybackState() {
    verifyApplicationThreadAndInitState();
    return state.playbackState;
  }

  @Override
  public final int getPlaybackSuppressionReason() {
    verifyApplicationThreadAndInitState();
    return state.playbackSuppressionReason;
  }

  @Nullable
  @Override
  public final PlaybackException getPlayerError() {
    verifyApplicationThreadAndInitState();
    return state.playerError;
  }

  @Override
  public final void setRepeatMode(@Player.RepeatMode int repeatMode) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_REPEAT_MODE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetRepeatMode(repeatMode),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setRepeatMode(repeatMode).build());
  }

  @Override
  @Player.RepeatMode
  public final int getRepeatMode() {
    verifyApplicationThreadAndInitState();
    return state.repeatMode;
  }

  @Override
  public final void setShuffleModeEnabled(boolean shuffleModeEnabled) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_SHUFFLE_MODE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetShuffleModeEnabled(shuffleModeEnabled),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setShuffleModeEnabled(shuffleModeEnabled).build());
  }

  @Override
  public final boolean getShuffleModeEnabled() {
    verifyApplicationThreadAndInitState();
    return state.shuffleModeEnabled;
  }

  @Override
  public final boolean isLoading() {
    verifyApplicationThreadAndInitState();
    return state.isLoading;
  }

  @Override
  @VisibleForTesting(otherwise = PROTECTED)
  public final void seekTo(
      int mediaItemIndex,
      long positionMs,
      @Player.Command int seekCommand,
      boolean isRepeatingCurrentItem) {
    verifyApplicationThreadAndInitState();
    checkArgument(mediaItemIndex == C.INDEX_UNSET || mediaItemIndex >= 0);
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(seekCommand)) {
      return;
    }
    boolean ignoreSeekForPlaceholderState =
        mediaItemIndex == C.INDEX_UNSET
            || isPlayingAd()
            || (!state.timeline.isEmpty() && mediaItemIndex >= state.timeline.getWindowCount());
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSeek(mediaItemIndex, positionMs, seekCommand),
        /* placeholderStateSupplier= */ () ->
            ignoreSeekForPlaceholderState
                ? state
                : getStateWithNewPlaylistAndPosition(
                    state, /* newPlaylist= */ null, mediaItemIndex, positionMs, window),
        /* forceSeekDiscontinuity= */ !ignoreSeekForPlaceholderState,
        isRepeatingCurrentItem);
  }

  @Override
  public final long getSeekBackIncrement() {
    verifyApplicationThreadAndInitState();
    return state.seekBackIncrementMs;
  }

  @Override
  public final long getSeekForwardIncrement() {
    verifyApplicationThreadAndInitState();
    return state.seekForwardIncrementMs;
  }

  @Override
  public final long getMaxSeekToPreviousPosition() {
    verifyApplicationThreadAndInitState();
    return state.maxSeekToPreviousPositionMs;
  }

  @Override
  public final void setPlaybackParameters(PlaybackParameters playbackParameters) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_SPEED_AND_PITCH)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetPlaybackParameters(playbackParameters),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setPlaybackParameters(playbackParameters).build());
  }

  @Override
  public final PlaybackParameters getPlaybackParameters() {
    verifyApplicationThreadAndInitState();
    return state.playbackParameters;
  }

  @Override
  public final void stop() {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_STOP)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleStop(),
        /* placeholderStateSupplier= */ () ->
            state
                .buildUpon()
                .setPlaybackState(Player.STATE_IDLE)
                .setTotalBufferedDurationMs(PositionSupplier.ZERO)
                .setContentBufferedPositionMs(
                    PositionSupplier.getConstant(getContentPositionMsInternal(state, window)))
                .setAdBufferedPositionMs(state.adPositionMsSupplier)
                .setIsLoading(false)
                .build());
  }

  @Override
  public final void release() {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_RELEASE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleRelease(), /* placeholderStateSupplier= */ () -> state);
    released = true;
    listeners.release();
    // Enforce some final state values in case getters are called after release.
    this.state =
        this.state
            .buildUpon()
            .setPlaybackState(Player.STATE_IDLE)
            .setTotalBufferedDurationMs(PositionSupplier.ZERO)
            .setContentBufferedPositionMs(
                PositionSupplier.getConstant(getContentPositionMsInternal(state, window)))
            .setAdBufferedPositionMs(state.adPositionMsSupplier)
            .setIsLoading(false)
            .build();
  }

  @Override
  public final Tracks getCurrentTracks() {
    verifyApplicationThreadAndInitState();
    return state.currentTracks;
  }

  @Override
  public final TrackSelectionParameters getTrackSelectionParameters() {
    verifyApplicationThreadAndInitState();
    return state.trackSelectionParameters;
  }

  @Override
  public final void setTrackSelectionParameters(TrackSelectionParameters parameters) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetTrackSelectionParameters(parameters),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setTrackSelectionParameters(parameters).build());
  }

  @Override
  public final MediaMetadata getMediaMetadata() {
    verifyApplicationThreadAndInitState();
    return state.currentMetadata;
  }

  @Override
  public final MediaMetadata getPlaylistMetadata() {
    verifyApplicationThreadAndInitState();
    return state.playlistMetadata;
  }

  @Override
  public final void setPlaylistMetadata(MediaMetadata mediaMetadata) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_PLAYLIST_METADATA)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetPlaylistMetadata(mediaMetadata),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setPlaylistMetadata(mediaMetadata).build());
  }

  @Override
  public final Timeline getCurrentTimeline() {
    verifyApplicationThreadAndInitState();
    return state.timeline;
  }

  @Override
  public final int getCurrentPeriodIndex() {
    verifyApplicationThreadAndInitState();
    return getCurrentPeriodIndexInternal(state, window, period);
  }

  @Override
  public final int getCurrentMediaItemIndex() {
    verifyApplicationThreadAndInitState();
    return getCurrentMediaItemIndexInternal(state);
  }

  @Override
  public final long getDuration() {
    verifyApplicationThreadAndInitState();
    if (isPlayingAd()) {
      state.timeline.getPeriod(getCurrentPeriodIndex(), period);
      long adDurationUs =
          period.getAdDurationUs(state.currentAdGroupIndex, state.currentAdIndexInAdGroup);
      return Util.usToMs(adDurationUs);
    }
    return getContentDuration();
  }

  @Override
  public final long getCurrentPosition() {
    verifyApplicationThreadAndInitState();
    return isPlayingAd() ? state.adPositionMsSupplier.get() : getContentPosition();
  }

  @Override
  public final long getBufferedPosition() {
    verifyApplicationThreadAndInitState();
    return isPlayingAd()
        ? max(state.adBufferedPositionMsSupplier.get(), state.adPositionMsSupplier.get())
        : getContentBufferedPosition();
  }

  @Override
  public final long getTotalBufferedDuration() {
    verifyApplicationThreadAndInitState();
    return state.totalBufferedDurationMsSupplier.get();
  }

  @Override
  public final boolean isPlayingAd() {
    verifyApplicationThreadAndInitState();
    return state.currentAdGroupIndex != C.INDEX_UNSET;
  }

  @Override
  public final int getCurrentAdGroupIndex() {
    verifyApplicationThreadAndInitState();
    return state.currentAdGroupIndex;
  }

  @Override
  public final int getCurrentAdIndexInAdGroup() {
    verifyApplicationThreadAndInitState();
    return state.currentAdIndexInAdGroup;
  }

  @Override
  public final long getContentPosition() {
    verifyApplicationThreadAndInitState();
    return getContentPositionMsInternal(state, window);
  }

  @Override
  public final long getContentBufferedPosition() {
    verifyApplicationThreadAndInitState();
    return max(
        getContentBufferedPositionMsInternal(state, window),
        getContentPositionMsInternal(state, window));
  }

  @Override
  public final AudioAttributes getAudioAttributes() {
    verifyApplicationThreadAndInitState();
    return state.audioAttributes;
  }

  @Override
  public final void setVolume(float volume) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VOLUME)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetVolume(volume),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setVolume(volume).build());
  }

  @Override
  public final float getVolume() {
    verifyApplicationThreadAndInitState();
    return state.volume;
  }

  @Override
  public final void setVideoSurface(@Nullable Surface surface) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) {
      return;
    }
    if (surface == null) {
      clearVideoSurface();
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetVideoOutput(surface),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setSurfaceSize(Size.UNKNOWN).build());
  }

  @Override
  public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) {
      return;
    }
    if (surfaceHolder == null) {
      clearVideoSurface();
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetVideoOutput(surfaceHolder),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setSurfaceSize(getSurfaceHolderSize(surfaceHolder)).build());
  }

  @Override
  public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) {
      return;
    }
    if (surfaceView == null) {
      clearVideoSurface();
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetVideoOutput(surfaceView),
        /* placeholderStateSupplier= */ () ->
            state
                .buildUpon()
                .setSurfaceSize(getSurfaceHolderSize(surfaceView.getHolder()))
                .build());
  }

  @Override
  public final void setVideoTextureView(@Nullable TextureView textureView) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) {
      return;
    }
    if (textureView == null) {
      clearVideoSurface();
      return;
    }
    Size surfaceSize;
    if (textureView.isAvailable()) {
      surfaceSize = new Size(textureView.getWidth(), textureView.getHeight());
    } else {
      surfaceSize = Size.ZERO;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetVideoOutput(textureView),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setSurfaceSize(surfaceSize).build());
  }

  @Override
  public final void clearVideoSurface() {
    clearVideoOutput(/* videoOutput= */ null);
  }

  @Override
  public final void clearVideoSurface(@Nullable Surface surface) {
    clearVideoOutput(surface);
  }

  @Override
  public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
    clearVideoOutput(surfaceHolder);
  }

  @Override
  public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
    clearVideoOutput(surfaceView);
  }

  @Override
  public final void clearVideoTextureView(@Nullable TextureView textureView) {
    clearVideoOutput(textureView);
  }

  private void clearVideoOutput(@Nullable Object videoOutput) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_VIDEO_SURFACE)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleClearVideoOutput(videoOutput),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setSurfaceSize(Size.ZERO).build());
  }

  @Override
  public final VideoSize getVideoSize() {
    verifyApplicationThreadAndInitState();
    return state.videoSize;
  }

  @Override
  public final Size getSurfaceSize() {
    verifyApplicationThreadAndInitState();
    return state.surfaceSize;
  }

  @Override
  public final CueGroup getCurrentCues() {
    verifyApplicationThreadAndInitState();
    return state.currentCues;
  }

  @Override
  public final DeviceInfo getDeviceInfo() {
    verifyApplicationThreadAndInitState();
    return state.deviceInfo;
  }

  @Override
  public final int getDeviceVolume() {
    verifyApplicationThreadAndInitState();
    return state.deviceVolume;
  }

  @Override
  public final boolean isDeviceMuted() {
    verifyApplicationThreadAndInitState();
    return state.isDeviceMuted;
  }

  /**
   * @deprecated Use {@link #setDeviceVolume(int, int)} instead.
   */
  @SuppressWarnings("deprecation") // Using deprecated command code
  @Deprecated
  @Override
  public final void setDeviceVolume(int volume) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_DEVICE_VOLUME)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetDeviceVolume(volume, C.VOLUME_FLAG_SHOW_UI),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build());
  }

  @Override
  public final void setDeviceVolume(int volume, @C.VolumeFlags int flags) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetDeviceVolume(volume, flags),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build());
  }

  /**
   * @deprecated Use {@link #increaseDeviceVolume(int)} instead.
   */
  @SuppressWarnings("deprecation") // Using deprecated command code
  @Deprecated
  @Override
  public final void increaseDeviceVolume() {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleIncreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build());
  }

  @Override
  public final void increaseDeviceVolume(@C.VolumeFlags int flags) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleIncreaseDeviceVolume(flags),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build());
  }

  /**
   * @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
   */
  @SuppressWarnings("deprecation") // Using deprecated command code
  @Deprecated
  @Override
  public final void decreaseDeviceVolume() {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleDecreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build());
  }

  @Override
  public final void decreaseDeviceVolume(@C.VolumeFlags int flags) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleDecreaseDeviceVolume(flags),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build());
  }

  /**
   * @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
   */
  @SuppressWarnings("deprecation") // Using deprecated command code
  @Deprecated
  @Override
  public final void setDeviceMuted(boolean muted) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetDeviceMuted(muted, C.VOLUME_FLAG_SHOW_UI),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build());
  }

  @Override
  public final void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetDeviceMuted(muted, flags),
        /* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).build());
  }

  @Override
  public final void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {
    verifyApplicationThreadAndInitState();
    // Use a local copy to ensure the lambda below uses the current state value.
    State state = this.state;
    if (!shouldHandleCommand(Player.COMMAND_SET_AUDIO_ATTRIBUTES)) {
      return;
    }
    updateStateForPendingOperation(
        /* pendingOperation= */ handleSetAudioAttributes(audioAttributes, handleAudioFocus),
        /* placeholderStateSupplier= */ () ->
            state.buildUpon().setAudioAttributes(audioAttributes).build());
  }

  /**
   * Invalidates the current state.
   *
   * <p>Triggers a call to {@link #getState()} and informs listeners if the state changed.
   *
   * <p>Note that this may not have an immediate effect while there are still player methods being
   * handled asynchronously. The state will be invalidated automatically once these pending
   * synchronous operations are finished and there is no need to call this method again.
   */
  protected final void invalidateState() {
    verifyApplicationThreadAndInitState();
    if (!pendingOperations.isEmpty() || released) {
      return;
    }
    updateStateAndInformListeners(
        getState(), /* forceSeekDiscontinuity= */ false, /* isRepeatingCurrentItem= */ false);
  }

  /**
   * Returns the current {@link State} of the player.
   *
   * <p>The {@link State} should include all {@linkplain
   * State.Builder#setAvailableCommands(Commands) available commands} indicating which player
   * methods are allowed to be called.
   *
   * <p>Note that this method won't be called while asynchronous handling of player methods is in
   * progress. This means that the implementation doesn't need to handle state changes caused by
   * these asynchronous operations until they are done and can return the currently known state
   * directly. The placeholder state used while these asynchronous operations are in progress can be
   * customized by overriding {@link #getPlaceholderState(State)} if required.
   */
  @ForOverride
  protected abstract State getState();

  /**
   * Returns the placeholder state used while a player method is handled asynchronously.
   *
   * <p>The {@code suggestedPlaceholderState} already contains the most likely state update, for
   * example setting {@link State#playWhenReady} to true if {@code player.setPlayWhenReady(true)} is
   * called, and an implementations only needs to override this method if it can determine a more
   * accurate placeholder state.
   *
   * @param suggestedPlaceholderState The suggested placeholder {@link State}, including the most
   *     likely outcome of handling all pending asynchronous operations.
   * @return The placeholder {@link State} to use while asynchronous operations are pending.
   */
  @ForOverride
  protected State getPlaceholderState(State suggestedPlaceholderState) {
    return suggestedPlaceholderState;
  }

  /**
   * Returns the placeholder {@link MediaItemData} used for a new {@link MediaItem} added to the
   * playlist.
   *
   * <p>An implementation only needs to override this method if it can determine a more accurate
   * placeholder state than the default.
   *
   * @param mediaItem The {@link MediaItem} added to the playlist.
   * @return The {@link MediaItemData} used as placeholder while adding the item to the playlist is
   *     in progress.
   */
  @ForOverride
  protected MediaItemData getPlaceholderMediaItemData(MediaItem mediaItem) {
    return new MediaItemData.Builder(new PlaceholderUid())
        .setMediaItem(mediaItem)
        .setIsDynamic(true)
        .setIsPlaceholder(true)
        .build();
  }

  /**
   * Handles calls to {@link Player#setPlayWhenReady}, {@link Player#play} and {@link Player#pause}.
   *
   * <p>Will only be called if {@link Player#COMMAND_PLAY_PAUSE} is available.
   *
   * @param playWhenReady The requested {@link State#playWhenReady}
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_PLAY_PAUSE");
  }

  /**
   * Handles calls to {@link Player#prepare}.
   *
   * <p>Will only be called if {@link Player#COMMAND_PREPARE} is available.
   *
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handlePrepare() {
    throw new IllegalStateException("Missing implementation to handle COMMAND_PREPARE");
  }

  /**
   * Handles calls to {@link Player#stop}.
   *
   * <p>Will only be called if {@link Player#COMMAND_STOP} is available.
   *
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleStop() {
    throw new IllegalStateException("Missing implementation to handle COMMAND_STOP");
  }

  /**
   * Handles calls to {@link Player#release}.
   *
   * <p>Will only be called if {@link Player#COMMAND_RELEASE} is available.
   *
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleRelease() {
    throw new IllegalStateException("Missing implementation to handle COMMAND_RELEASE");
  }

  /**
   * Handles calls to {@link Player#setRepeatMode}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_REPEAT_MODE} is available.
   *
   * @param repeatMode The requested {@link RepeatMode}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetRepeatMode(@RepeatMode int repeatMode) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_REPEAT_MODE");
  }

  /**
   * Handles calls to {@link Player#setShuffleModeEnabled}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_SHUFFLE_MODE} is available.
   *
   * @param shuffleModeEnabled Whether shuffle mode was requested to be enabled.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetShuffleModeEnabled(boolean shuffleModeEnabled) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_SHUFFLE_MODE");
  }

  /**
   * Handles calls to {@link Player#setPlaybackParameters} or {@link Player#setPlaybackSpeed}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_SPEED_AND_PITCH} is available.
   *
   * @param playbackParameters The requested {@link PlaybackParameters}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetPlaybackParameters(PlaybackParameters playbackParameters) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_SPEED_AND_PITCH");
  }

  /**
   * Handles calls to {@link Player#setTrackSelectionParameters}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_TRACK_SELECTION_PARAMETERS} is available.
   *
   * @param trackSelectionParameters The requested {@link TrackSelectionParameters}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetTrackSelectionParameters(
      TrackSelectionParameters trackSelectionParameters) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_SET_TRACK_SELECTION_PARAMETERS");
  }

  /**
   * Handles calls to {@link Player#setPlaylistMetadata}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_PLAYLIST_METADATA} is available.
   *
   * @param playlistMetadata The requested {@linkplain MediaMetadata playlist metadata}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetPlaylistMetadata(MediaMetadata playlistMetadata) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_SET_PLAYLIST_METADATA");
  }

  /**
   * Handles calls to {@link Player#setVolume}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_VOLUME} is available.
   *
   * @param volume The requested audio volume, with 0 being silence and 1 being unity gain (signal
   *     unchanged).
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetVolume(@FloatRange(from = 0, to = 1.0) float volume) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VOLUME");
  }

  /**
   * Handles calls to {@link Player#setDeviceVolume(int)} and {@link Player#setDeviceVolume(int,
   * int)}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} or {@link
   * Player#COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS} is available.
   *
   * @param deviceVolume The requested device volume.
   * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetDeviceVolume(
      @IntRange(from = 0) int deviceVolume, int flags) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_SET_DEVICE_VOLUME or"
            + " COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS");
  }

  /**
   * Handles calls to {@link Player#increaseDeviceVolume()} and {@link
   * Player#increaseDeviceVolume(int)}.
   *
   * <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
   * Player#COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is available.
   *
   * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleIncreaseDeviceVolume(@C.VolumeFlags int flags) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
            + " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
  }

  /**
   * Handles calls to {@link Player#decreaseDeviceVolume()} and {@link
   * Player#decreaseDeviceVolume(int)}.
   *
   * <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
   * Player#COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is available.
   *
   * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleDecreaseDeviceVolume(@C.VolumeFlags int flags) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
            + " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
  }

  /**
   * Handles calls to {@link Player#setDeviceMuted(boolean)} and {@link
   * Player#setDeviceMuted(boolean, int)}.
   *
   * <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} or {@link
   * Player#COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS} is available.
   *
   * @param muted Whether the device was requested to be muted.
   * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME or"
            + " COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS");
  }

  /**
   * Handles calls to set the audio attributes.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_AUDIO_ATTRIBUTES} is available.
   *
   * @param audioAttributes The attributes to use for audio playback.
   * @param handleAudioFocus True if the player should handle audio focus, false otherwise.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetAudioAttributes(
      AudioAttributes audioAttributes, boolean handleAudioFocus) {
    throw new IllegalStateException(
        "Missing implementation to handle COMMAND_SET_AUDIO_ATTRIBUTES");
  }

  /**
   * Handles calls to set the video output.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available.
   *
   * @param videoOutput The requested video output. This is either a {@link Surface}, {@link
   *     SurfaceHolder}, {@link TextureView} or {@link SurfaceView}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetVideoOutput(Object videoOutput) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VIDEO_SURFACE");
  }

  /**
   * Handles calls to clear the video output.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_VIDEO_SURFACE} is available.
   *
   * @param videoOutput The video output to clear. If null any current output should be cleared. If
   *     non-null, the output should only be cleared if it matches the provided argument. This is
   *     either a {@link Surface}, {@link SurfaceHolder}, {@link TextureView} or {@link
   *     SurfaceView}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleClearVideoOutput(@Nullable Object videoOutput) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_VIDEO_SURFACE");
  }

  /**
   * Handles calls to {@link Player#setMediaItem} and {@link Player#setMediaItems}.
   *
   * <p>Will only be called if {@link Player#COMMAND_SET_MEDIA_ITEM} or {@link
   * Player#COMMAND_CHANGE_MEDIA_ITEMS} is available. If only {@link Player#COMMAND_SET_MEDIA_ITEM}
   * is available, the list of media items will always contain exactly one item.
   *
   * @param mediaItems The media items to add.
   * @param startIndex The index at which to start playback from, or {@link C#INDEX_UNSET} to start
   *     at the default item.
   * @param startPositionMs The position in milliseconds to start playback from, or {@link
   *     C#TIME_UNSET} to start at the default position in the media item.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSetMediaItems(
      List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_SET_MEDIA_ITEM(S)");
  }

  /**
   * Handles calls to {@link Player#addMediaItem} and {@link Player#addMediaItems}.
   *
   * <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
   *
   * @param index The index at which to add the items. The index is in the range 0 &lt;= {@code
   *     index} &lt;= {@link #getMediaItemCount()}.
   * @param mediaItems The media items to add.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleAddMediaItems(int index, List<MediaItem> mediaItems) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS");
  }

  /**
   * Handles calls to {@link Player#moveMediaItem} and {@link Player#moveMediaItems}.
   *
   * <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
   *
   * @param fromIndex The start index of the items to move. The index is in the range 0 &lt;= {@code
   *     fromIndex} &lt; {@link #getMediaItemCount()}.
   * @param toIndex The index of the first item not to be included in the move (exclusive). The
   *     index is in the range {@code fromIndex} &lt; {@code toIndex} &lt;= {@link
   *     #getMediaItemCount()}.
   * @param newIndex The new index of the first moved item. The index is in the range {@code 0}
   *     &lt;= {@code newIndex} &lt; {@link #getMediaItemCount() - (toIndex - fromIndex)}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS");
  }

  /**
   * Handles calls to {@link Player#replaceMediaItem} and {@link Player#replaceMediaItems}.
   *
   * <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
   *
   * @param fromIndex The start index of the items to replace. The index is in the range 0 &lt;=
   *     {@code fromIndex} &lt; {@link #getMediaItemCount()}.
   * @param toIndex The index of the first item not to be replaced (exclusive). The index is in the
   *     range {@code fromIndex} &lt; {@code toIndex} &lt;= {@link #getMediaItemCount()}.
   * @param mediaItems The media items to replace the specified range with.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleReplaceMediaItems(
      int fromIndex, int toIndex, List<MediaItem> mediaItems) {
    ListenableFuture<?> addFuture = handleAddMediaItems(toIndex, mediaItems);
    ListenableFuture<?> removeFuture = handleRemoveMediaItems(fromIndex, toIndex);
    return Util.transformFutureAsync(addFuture, unused -> removeFuture);
  }

  /**
   * Handles calls to {@link Player#removeMediaItem} and {@link Player#removeMediaItems}.
   *
   * <p>Will only be called if {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} is available.
   *
   * @param fromIndex The index at which to start removing media items. The index is in the range 0
   *     &lt;= {@code fromIndex} &lt; {@link #getMediaItemCount()}.
   * @param toIndex The index of the first item to be kept (exclusive). The index is in the range
   *     {@code fromIndex} &lt; {@code toIndex} &lt;= {@link #getMediaItemCount()}.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleRemoveMediaItems(int fromIndex, int toIndex) {
    throw new IllegalStateException("Missing implementation to handle COMMAND_CHANGE_MEDIA_ITEMS");
  }

  /**
   * Handles calls to {@link Player#seekTo} and other seek operations (for example, {@link
   * Player#seekToNext}).
   *
   * <p>Will only be called if the appropriate {@link Player.Command}, for example {@link
   * Player#COMMAND_SEEK_TO_MEDIA_ITEM} or {@link Player#COMMAND_SEEK_TO_NEXT}, is available.
   *
   * @param mediaItemIndex The media item index to seek to. If the original seek operation did not
   *     directly specify an index, this is the most likely implied index based on the available
   *     player state. If the implied action is to do nothing, this will be {@link C#INDEX_UNSET}.
   * @param positionMs The position in milliseconds to start playback from, or {@link C#TIME_UNSET}
   *     to start at the default position in the media item. If the original seek operation did not
   *     directly specify a position, this is the most likely implied position based on the
   *     available player state.
   * @param seekCommand The {@link Player.Command} used to trigger the seek.
   * @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
   *     changes caused by this call.
   */
  @ForOverride
  protected ListenableFuture<?> handleSeek(
      int mediaItemIndex, long positionMs, @Player.Command int seekCommand) {
    throw new IllegalStateException("Missing implementation to handle one of the COMMAND_SEEK_*");
  }

  /**
   * Throws an {@link IllegalStateException} if the the thread calling this method does not match
   * the {@link Looper} thread that was specified upon construction of this instance.
   *
   * <p>Subclasses can use this method to verify that their own defined methods are also accessed by
   * the correct thread.
   */
  protected final void verifyApplicationThread() {
    if (Thread.currentThread() != applicationLooper.getThread()) {
      String message =
          Util.formatInvariant(
              "Player is accessed on the wrong thread.\n"
                  + "Current thread: '%s'\n"
                  + "Expected thread: '%s'\n",
              Thread.currentThread().getName(), applicationLooper.getThread().getName());
      throw new IllegalStateException(message);
    }
  }

  @RequiresNonNull("state")
  private boolean shouldHandleCommand(@Player.Command int commandCode) {
    return !released && state.availableCommands.contains(commandCode);
  }

  @SuppressWarnings("deprecation") // Calling deprecated listener methods.
  @RequiresNonNull("state")
  private void updateStateAndInformListeners(
      State newState, boolean forceSeekDiscontinuity, boolean isRepeatingCurrentItem) {
    State previousState = state;
    // Assign new state immediately such that all getters return the right values, but use a
    // snapshot of the previous and new state so that listener invocations are triggered correctly.
    this.state = newState;
    if (newState.hasPositionDiscontinuity || newState.newlyRenderedFirstFrame) {
      // Clear one-time events to avoid signalling them again later.
      this.state =
          this.state
              .buildUpon()
              .clearPositionDiscontinuity()
              .setNewlyRenderedFirstFrame(false)
              .build();
    }

    boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady;
    boolean playbackStateChanged = previousState.playbackState != newState.playbackState;
    int positionDiscontinuityReason =
        getPositionDiscontinuityReason(
            previousState, newState, forceSeekDiscontinuity, window, period);
    boolean timelineChanged = !previousState.timeline.equals(newState.timeline);
    int mediaItemTransitionReason =
        getMediaItemTransitionReason(
            previousState, newState, positionDiscontinuityReason, isRepeatingCurrentItem, window);

    if (timelineChanged) {
      @Player.TimelineChangeReason
      int timelineChangeReason =
          getTimelineChangeReason(previousState.timeline, newState.timeline, window);
      listeners.queueEvent(
          Player.EVENT_TIMELINE_CHANGED,
          listener -> listener.onTimelineChanged(newState.timeline, timelineChangeReason));
    }
    if (positionDiscontinuityReason != C.INDEX_UNSET) {
      PositionInfo previousPositionInfo =
          getPositionInfo(previousState, /* useDiscontinuityPosition= */ false, window, period);
      PositionInfo positionInfo =
          getPositionInfo(
              newState,
              /* useDiscontinuityPosition= */ newState.hasPositionDiscontinuity,
              window,
              period);
      listeners.queueEvent(
          Player.EVENT_POSITION_DISCONTINUITY,
          listener -> {
            listener.onPositionDiscontinuity(positionDiscontinuityReason);
            listener.onPositionDiscontinuity(
                previousPositionInfo, positionInfo, positionDiscontinuityReason);
          });
    }
    if (mediaItemTransitionReason != C.INDEX_UNSET) {
      @Nullable
      MediaItem mediaItem =
          newState.timeline.isEmpty()
              ? null
              : newState.timeline.getWindow(getCurrentMediaItemIndexInternal(newState), window)
                  .mediaItem;
      listeners.queueEvent(
          Player.EVENT_MEDIA_ITEM_TRANSITION,
          listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
    }
    if (!Util.areEqual(previousState.playerError, newState.playerError)) {
      listeners.queueEvent(
          Player.EVENT_PLAYER_ERROR,
          listener -> listener.onPlayerErrorChanged(newState.playerError));
      if (newState.playerError != null) {
        listeners.queueEvent(
            Player.EVENT_PLAYER_ERROR,
            listener -> listener.onPlayerError(castNonNull(newState.playerError)));
      }
    }
    if (!previousState.trackSelectionParameters.equals(newState.trackSelectionParameters)) {
      listeners.queueEvent(
          Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
          listener ->
              listener.onTrackSelectionParametersChanged(newState.trackSelectionParameters));
    }
    if (!previousState.currentTracks.equals(newState.currentTracks)) {
      listeners.queueEvent(
          Player.EVENT_TRACKS_CHANGED,
          listener -> listener.onTracksChanged(newState.currentTracks));
    }
    if (!previousState.currentMetadata.equals(newState.currentMetadata)) {
      listeners.queueEvent(
          EVENT_MEDIA_METADATA_CHANGED,
          listener -> listener.onMediaMetadataChanged(newState.currentMetadata));
    }
    if (previousState.isLoading != newState.isLoading) {
      listeners.queueEvent(
          Player.EVENT_IS_LOADING_CHANGED,
          listener -> {
            listener.onLoadingChanged(newState.isLoading);
            listener.onIsLoadingChanged(newState.isLoading);
          });
    }
    if (playWhenReadyChanged || playbackStateChanged) {
      listeners.queueEvent(
          /* eventFlag= */ C.INDEX_UNSET,
          listener ->
              listener.onPlayerStateChanged(newState.playWhenReady, newState.playbackState));
    }
    if (playbackStateChanged) {
      listeners.queueEvent(
          Player.EVENT_PLAYBACK_STATE_CHANGED,
          listener -> listener.onPlaybackStateChanged(newState.playbackState));
    }
    if (playWhenReadyChanged
        || previousState.playWhenReadyChangeReason != newState.playWhenReadyChangeReason) {
      listeners.queueEvent(
          Player.EVENT_PLAY_WHEN_READY_CHANGED,
          listener ->
              listener.onPlayWhenReadyChanged(
                  newState.playWhenReady, newState.playWhenReadyChangeReason));
    }
    if (previousState.playbackSuppressionReason != newState.playbackSuppressionReason) {
      listeners.queueEvent(
          Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
          listener ->
              listener.onPlaybackSuppressionReasonChanged(newState.playbackSuppressionReason));
    }
    if (isPlaying(previousState) != isPlaying(newState)) {
      listeners.queueEvent(
          Player.EVENT_IS_PLAYING_CHANGED,
          listener -> listener.onIsPlayingChanged(isPlaying(newState)));
    }
    if (!previousState.playbackParameters.equals(newState.playbackParameters)) {
      listeners.queueEvent(
          Player.EVENT_PLAYBACK_PARAMETERS_CHANGED,
          listener -> listener.onPlaybackParametersChanged(newState.playbackParameters));
    }
    if (previousState.repeatMode != newState.repeatMode) {
      listeners.queueEvent(
          Player.EVENT_REPEAT_MODE_CHANGED,
          listener -> listener.onRepeatModeChanged(newState.repeatMode));
    }
    if (previousState.shuffleModeEnabled != newState.shuffleModeEnabled) {
      listeners.queueEvent(
          Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
          listener -> listener.onShuffleModeEnabledChanged(newState.shuffleModeEnabled));
    }
    if (previousState.seekBackIncrementMs != newState.seekBackIncrementMs) {
      listeners.queueEvent(
          Player.EVENT_SEEK_BACK_INCREMENT_CHANGED,
          listener -> listener.onSeekBackIncrementChanged(newState.seekBackIncrementMs));
    }
    if (previousState.seekForwardIncrementMs != newState.seekForwardIncrementMs) {
      listeners.queueEvent(
          Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
          listener -> listener.onSeekForwardIncrementChanged(newState.seekForwardIncrementMs));
    }
    if (previousState.maxSeekToPreviousPositionMs != newState.maxSeekToPreviousPositionMs) {
      listeners.queueEvent(
          Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED,
          listener ->
              listener.onMaxSeekToPreviousPositionChanged(newState.maxSeekToPreviousPositionMs));
    }
    if (!previousState.audioAttributes.equals(newState.audioAttributes)) {
      listeners.queueEvent(
          Player.EVENT_AUDIO_ATTRIBUTES_CHANGED,
          listener -> listener.onAudioAttributesChanged(newState.audioAttributes));
    }
    if (!previousState.videoSize.equals(newState.videoSize)) {
      listeners.queueEvent(
          Player.EVENT_VIDEO_SIZE_CHANGED,
          listener -> listener.onVideoSizeChanged(newState.videoSize));
    }
    if (!previousState.deviceInfo.equals(newState.deviceInfo)) {
      listeners.queueEvent(
          Player.EVENT_DEVICE_INFO_CHANGED,
          listener -> listener.onDeviceInfoChanged(newState.deviceInfo));
    }
    if (!previousState.playlistMetadata.equals(newState.playlistMetadata)) {
      listeners.queueEvent(
          Player.EVENT_PLAYLIST_METADATA_CHANGED,
          listener -> listener.onPlaylistMetadataChanged(newState.playlistMetadata));
    }
    if (newState.newlyRenderedFirstFrame) {
      listeners.queueEvent(Player.EVENT_RENDERED_FIRST_FRAME, Listener::onRenderedFirstFrame);
    }
    if (!previousState.surfaceSize.equals(newState.surfaceSize)) {
      listeners.queueEvent(
          Player.EVENT_SURFACE_SIZE_CHANGED,
          listener ->
              listener.onSurfaceSizeChanged(
                  newState.surfaceSize.getWidth(), newState.surfaceSize.getHeight()));
    }
    if (previousState.volume != newState.volume) {
      listeners.queueEvent(
          Player.EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(newState.volume));
    }
    if (previousState.deviceVolume != newState.deviceVolume
        || previousState.isDeviceMuted != newState.isDeviceMuted) {
      listeners.queueEvent(
          Player.EVENT_DEVICE_VOLUME_CHANGED,
          listener ->
              listener.onDeviceVolumeChanged(newState.deviceVolume, newState.isDeviceMuted));
    }
    if (!previousState.currentCues.equals(newState.currentCues)) {
      listeners.queueEvent(
          Player.EVENT_CUES,
          listener -> {
            listener.onCues(newState.currentCues.cues);
            listener.onCues(newState.currentCues);
          });
    }
    if (!previousState.timedMetadata.equals(newState.timedMetadata)
        && newState.timedMetadata.presentationTimeUs != C.TIME_UNSET) {
      listeners.queueEvent(
          Player.EVENT_METADATA, listener -> listener.onMetadata(newState.timedMetadata));
    }
    if (!previousState.availableCommands.equals(newState.availableCommands)) {
      listeners.queueEvent(
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
          listener -> listener.onAvailableCommandsChanged(newState.availableCommands));
    }
    listeners.flushEvents();
  }

  @EnsuresNonNull("state")
  private void verifyApplicationThreadAndInitState() {
    verifyApplicationThread();
    if (state == null) {
      // First time accessing state.
      state = getState();
    }
  }

  @RequiresNonNull("state")
  private void updateStateForPendingOperation(
      ListenableFuture<?> pendingOperation, Supplier<State> placeholderStateSupplier) {
    updateStateForPendingOperation(
        pendingOperation,
        placeholderStateSupplier,
        /* forceSeekDiscontinuity= */ false,
        /* isRepeatingCurrentItem= */ false);
  }

  @RequiresNonNull("state")
  private void updateStateForPendingOperation(
      ListenableFuture<?> pendingOperation,
      Supplier<State> placeholderStateSupplier,
      boolean forceSeekDiscontinuity,
      boolean isRepeatingCurrentItem) {
    if (pendingOperation.isDone() && pendingOperations.isEmpty()) {
      updateStateAndInformListeners(getState(), forceSeekDiscontinuity, isRepeatingCurrentItem);
    } else {
      pendingOperations.add(pendingOperation);
      State suggestedPlaceholderState = placeholderStateSupplier.get();
      updateStateAndInformListeners(
          getPlaceholderState(suggestedPlaceholderState),
          forceSeekDiscontinuity,
          isRepeatingCurrentItem);
      pendingOperation.addListener(
          () -> {
            castNonNull(state); // Already checked by method @RequiresNonNull pre-condition.
            pendingOperations.remove(pendingOperation);
            if (pendingOperations.isEmpty() && !released) {
              updateStateAndInformListeners(
                  getState(),
                  /* forceSeekDiscontinuity= */ false,
                  /* isRepeatingCurrentItem= */ false);
            }
          },
          this::postOrRunOnApplicationHandler);
    }
  }

  private void postOrRunOnApplicationHandler(Runnable runnable) {
    if (applicationHandler.getLooper() == Looper.myLooper()) {
      runnable.run();
    } else {
      applicationHandler.post(runnable);
    }
  }

  private static boolean isPlaying(State state) {
    return state.playWhenReady
        && state.playbackState == Player.STATE_READY
        && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
  }

  private static int getCurrentMediaItemIndexInternal(State state) {
    if (state.currentMediaItemIndex != C.INDEX_UNSET) {
      return state.currentMediaItemIndex;
    }
    return 0; // TODO: Use shuffle order to get first item if playlist is not empty.
  }

  private static long getContentPositionMsInternal(State state, Timeline.Window window) {
    return getPositionOrDefaultInMediaItem(state.contentPositionMsSupplier.get(), state, window);
  }

  private static long getContentBufferedPositionMsInternal(State state, Timeline.Window window) {
    return getPositionOrDefaultInMediaItem(
        state.contentBufferedPositionMsSupplier.get(), state, window);
  }

  private static long getPositionOrDefaultInMediaItem(
      long positionMs, State state, Timeline.Window window) {
    if (positionMs != C.TIME_UNSET) {
      return positionMs;
    }
    if (state.timeline.isEmpty()) {
      return 0;
    }
    return state
        .timeline
        .getWindow(getCurrentMediaItemIndexInternal(state), window)
        .getDefaultPositionMs();
  }

  private static int getCurrentPeriodIndexInternal(
      State state, Timeline.Window window, Timeline.Period period) {
    int currentMediaItemIndex = getCurrentMediaItemIndexInternal(state);
    if (state.timeline.isEmpty()) {
      return currentMediaItemIndex;
    }
    return getPeriodIndexFromWindowPosition(
        state.timeline,
        currentMediaItemIndex,
        getContentPositionMsInternal(state, window),
        window,
        period);
  }

  private static int getPeriodIndexFromWindowPosition(
      Timeline timeline,
      int windowIndex,
      long windowPositionMs,
      Timeline.Window window,
      Timeline.Period period) {
    Object periodUid =
        timeline.getPeriodPositionUs(window, period, windowIndex, msToUs(windowPositionMs)).first;
    return timeline.getIndexOfPeriod(periodUid);
  }

  private static @Player.TimelineChangeReason int getTimelineChangeReason(
      Timeline previousTimeline, Timeline newTimeline, Timeline.Window window) {
    if (previousTimeline.getWindowCount() != newTimeline.getWindowCount()) {
      return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
    }
    for (int i = 0; i < previousTimeline.getWindowCount(); i++) {
      Object previousUid = previousTimeline.getWindow(/* windowIndex= */ i, window).uid;
      Object newUid = newTimeline.getWindow(/* windowIndex= */ i, window).uid;
      boolean resolvedAutoGeneratedPlaceholder =
          previousUid instanceof PlaceholderUid && !(newUid instanceof PlaceholderUid);
      if (!previousUid.equals(newUid) && !resolvedAutoGeneratedPlaceholder) {
        return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
      }
    }
    return Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE;
  }

  private static int getPositionDiscontinuityReason(
      State previousState,
      State newState,
      boolean forceSeekDiscontinuity,
      Timeline.Window window,
      Timeline.Period period) {
    if (newState.hasPositionDiscontinuity) {
      // We were asked to report a discontinuity.
      return newState.positionDiscontinuityReason;
    }
    if (forceSeekDiscontinuity) {
      return Player.DISCONTINUITY_REASON_SEEK;
    }
    if (previousState.timeline.isEmpty()) {
      // First change from an empty playlist is not reported as a discontinuity.
      return C.INDEX_UNSET;
    }
    if (newState.timeline.isEmpty()) {
      // The playlist became empty.
      return Player.DISCONTINUITY_REASON_REMOVE;
    }
    Object previousPeriodUid =
        previousState.timeline.getUidOfPeriod(
            getCurrentPeriodIndexInternal(previousState, window, period));
    Object newPeriodUid =
        newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window, period));
    if (previousPeriodUid instanceof PlaceholderUid && !(newPeriodUid instanceof PlaceholderUid)) {
      // An auto-generated placeholder was resolved to a real item.
      return C.INDEX_UNSET;
    }
    if (!newPeriodUid.equals(previousPeriodUid)
        || previousState.currentAdGroupIndex != newState.currentAdGroupIndex
        || previousState.currentAdIndexInAdGroup != newState.currentAdIndexInAdGroup) {
      // The current period or ad inside a period changed.
      if (newState.timeline.getIndexOfPeriod(previousPeriodUid) == C.INDEX_UNSET) {
        // The previous period no longer exists.
        return Player.DISCONTINUITY_REASON_REMOVE;
      }
      // Check if reached the previous period's or ad's duration to assume an auto-transition.
      long previousPositionMs =
          getCurrentPeriodOrAdPositionMs(previousState, previousPeriodUid, period, window);
      long previousDurationMs = getPeriodOrAdDurationMs(previousState, previousPeriodUid, period);
      return previousDurationMs != C.TIME_UNSET && previousPositionMs >= previousDurationMs
          ? Player.DISCONTINUITY_REASON_AUTO_TRANSITION
          : Player.DISCONTINUITY_REASON_SKIP;
    }
    // We are in the same content period or ad. Check if the position deviates more than a
    // reasonable threshold from the previous one.
    long previousPositionMs =
        getCurrentPeriodOrAdPositionMs(previousState, previousPeriodUid, period, window);
    long newPositionMs = getCurrentPeriodOrAdPositionMs(newState, newPeriodUid, period, window);
    if (Math.abs(previousPositionMs - newPositionMs) < POSITION_DISCONTINUITY_THRESHOLD_MS) {
      return C.INDEX_UNSET;
    }
    // Check if we previously reached the end of the item to assume an auto-repetition.
    long previousDurationMs = getPeriodOrAdDurationMs(previousState, previousPeriodUid, period);
    return previousDurationMs != C.TIME_UNSET && previousPositionMs >= previousDurationMs
        ? Player.DISCONTINUITY_REASON_AUTO_TRANSITION
        : Player.DISCONTINUITY_REASON_INTERNAL;
  }

  private static long getCurrentPeriodOrAdPositionMs(
      State state, Object currentPeriodUid, Timeline.Period period, Timeline.Window window) {
    return state.currentAdGroupIndex != C.INDEX_UNSET
        ? state.adPositionMsSupplier.get()
        : getContentPositionMsInternal(state, window)
            - state.timeline.getPeriodByUid(currentPeriodUid, period).getPositionInWindowMs();
  }

  private static long getPeriodOrAdDurationMs(
      State state, Object currentPeriodUid, Timeline.Period period) {
    state.timeline.getPeriodByUid(currentPeriodUid, period);
    long periodOrAdDurationUs =
        state.currentAdGroupIndex == C.INDEX_UNSET
            ? period.durationUs
            : period.getAdDurationUs(state.currentAdGroupIndex, state.currentAdIndexInAdGroup);
    return usToMs(periodOrAdDurationUs);
  }

  private static PositionInfo getPositionInfo(
      State state,
      boolean useDiscontinuityPosition,
      Timeline.Window window,
      Timeline.Period period) {
    @Nullable Object windowUid = null;
    @Nullable Object periodUid = null;
    int mediaItemIndex = getCurrentMediaItemIndexInternal(state);
    int periodIndex = C.INDEX_UNSET;
    @Nullable MediaItem mediaItem = null;
    if (!state.timeline.isEmpty()) {
      periodIndex = getCurrentPeriodIndexInternal(state, window, period);
      periodUid = state.timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid;
      windowUid = state.timeline.getWindow(mediaItemIndex, window).uid;
      mediaItem = window.mediaItem;
    }
    long contentPositionMs;
    long positionMs;
    if (useDiscontinuityPosition) {
      positionMs = state.discontinuityPositionMs;
      contentPositionMs =
          state.currentAdGroupIndex == C.INDEX_UNSET
              ? positionMs
              : getContentPositionMsInternal(state, window);
    } else {
      contentPositionMs = getContentPositionMsInternal(state, window);
      positionMs =
          state.currentAdGroupIndex != C.INDEX_UNSET
              ? state.adPositionMsSupplier.get()
              : contentPositionMs;
    }
    return new PositionInfo(
        windowUid,
        mediaItemIndex,
        mediaItem,
        periodUid,
        periodIndex,
        positionMs,
        contentPositionMs,
        state.currentAdGroupIndex,
        state.currentAdIndexInAdGroup);
  }

  private static int getMediaItemTransitionReason(
      State previousState,
      State newState,
      int positionDiscontinuityReason,
      boolean isRepeatingCurrentItem,
      Timeline.Window window) {
    Timeline previousTimeline = previousState.timeline;
    Timeline newTimeline = newState.timeline;
    if (newTimeline.isEmpty() && previousTimeline.isEmpty()) {
      return C.INDEX_UNSET;
    } else if (newTimeline.isEmpty() != previousTimeline.isEmpty()) {
      return MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
    }
    Object previousWindowUid =
        previousState.timeline.getWindow(getCurrentMediaItemIndexInternal(previousState), window)
            .uid;
    Object newWindowUid =
        newState.timeline.getWindow(getCurrentMediaItemIndexInternal(newState), window).uid;
    if (previousWindowUid instanceof PlaceholderUid && !(newWindowUid instanceof PlaceholderUid)) {
      // An auto-generated placeholder was resolved to a real item.
      return C.INDEX_UNSET;
    }
    if (!previousWindowUid.equals(newWindowUid)) {
      if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
        return MEDIA_ITEM_TRANSITION_REASON_AUTO;
      } else if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK) {
        return MEDIA_ITEM_TRANSITION_REASON_SEEK;
      } else {
        return MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
      }
    }
    // Only mark changes within the current item as a transition if we are repeating automatically
    // or via a seek to next/previous.
    if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
      if ((getContentPositionMsInternal(previousState, window)
              > getContentPositionMsInternal(newState, window))
          || (newState.hasPositionDiscontinuity
              && newState.discontinuityPositionMs == C.TIME_UNSET
              && isRepeatingCurrentItem)) {
        return MEDIA_ITEM_TRANSITION_REASON_REPEAT;
      }
    }
    if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK && isRepeatingCurrentItem) {
      return MEDIA_ITEM_TRANSITION_REASON_SEEK;
    }
    return C.INDEX_UNSET;
  }

  private static Size getSurfaceHolderSize(SurfaceHolder surfaceHolder) {
    if (!surfaceHolder.getSurface().isValid()) {
      return Size.ZERO;
    }
    Rect surfaceFrame = surfaceHolder.getSurfaceFrame();
    return new Size(surfaceFrame.width(), surfaceFrame.height());
  }

  private static int getMediaItemIndexInNewPlaylist(
      Timeline oldTimeline,
      Timeline newTimeline,
      int oldMediaItemIndex,
      Timeline.Period period,
      Timeline.Window window) {
    if (oldTimeline.isEmpty()) {
      return oldMediaItemIndex < newTimeline.getWindowCount() ? oldMediaItemIndex : C.INDEX_UNSET;
    }
    int oldFirstPeriodIndex = oldTimeline.getWindow(oldMediaItemIndex, window).firstPeriodIndex;
    Object oldFirstPeriodUid =
        checkNotNull(oldTimeline.getPeriod(oldFirstPeriodIndex, period, /* setIds= */ true).uid);
    if (newTimeline.getIndexOfPeriod(oldFirstPeriodUid) == C.INDEX_UNSET) {
      return C.INDEX_UNSET;
    }
    return newTimeline.getPeriodByUid(oldFirstPeriodUid, period).windowIndex;
  }

  private static State getStateWithNewPlaylist(
      State oldState,
      List<MediaItemData> newPlaylist,
      Timeline.Period period,
      Timeline.Window window) {
    State.Builder stateBuilder = oldState.buildUpon();
    Timeline newTimeline = new PlaylistTimeline(newPlaylist);
    Timeline oldTimeline = oldState.timeline;
    long oldPositionMs = oldState.contentPositionMsSupplier.get();
    int oldIndex = getCurrentMediaItemIndexInternal(oldState);
    int newIndex =
        getMediaItemIndexInNewPlaylist(oldTimeline, newTimeline, oldIndex, period, window);
    long newPositionMs = newIndex == C.INDEX_UNSET ? C.TIME_UNSET : oldPositionMs;
    // If the current item no longer exists, try to find a matching subsequent item.
    for (int i = oldIndex + 1; newIndex == C.INDEX_UNSET && i < oldTimeline.getWindowCount(); i++) {
      // TODO: Use shuffle order to iterate.
      newIndex =
          getMediaItemIndexInNewPlaylist(
              oldTimeline, newTimeline, /* oldMediaItemIndex= */ i, period, window);
    }
    // If this fails, transition to ENDED state.
    if (oldState.playbackState != Player.STATE_IDLE && newIndex == C.INDEX_UNSET) {
      stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false);
    }
    return buildStateForNewPosition(
        stateBuilder,
        oldState,
        oldPositionMs,
        newTimeline,
        newIndex,
        newPositionMs,
        /* keepAds= */ true,
        window);
  }

  private static State getStateWithNewPlaylistAndPosition(
      State oldState,
      @Nullable List<MediaItemData> newPlaylist,
      int newIndex,
      long newPositionMs,
      Timeline.Window window) {
    State.Builder stateBuilder = oldState.buildUpon();
    Timeline newTimeline =
        newPlaylist == null ? oldState.timeline : new PlaylistTimeline(newPlaylist);
    if (oldState.playbackState != Player.STATE_IDLE) {
      if (newTimeline.isEmpty()
          || (newIndex != C.INDEX_UNSET && newIndex >= newTimeline.getWindowCount())) {
        stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false);
      } else {
        stateBuilder.setPlaybackState(Player.STATE_BUFFERING);
      }
    }
    long oldPositionMs = oldState.contentPositionMsSupplier.get();
    return buildStateForNewPosition(
        stateBuilder,
        oldState,
        oldPositionMs,
        newTimeline,
        newIndex,
        newPositionMs,
        /* keepAds= */ false,
        window);
  }

  private static State buildStateForNewPosition(
      State.Builder stateBuilder,
      State oldState,
      long oldPositionMs,
      Timeline newTimeline,
      int newIndex,
      long newPositionMs,
      boolean keepAds,
      Timeline.Window window) {
    // Resolve unset or invalid index and position.
    oldPositionMs = getPositionOrDefaultInMediaItem(oldPositionMs, oldState, window);
    if (!newTimeline.isEmpty()
        && (newIndex == C.INDEX_UNSET || newIndex >= newTimeline.getWindowCount())) {
      newIndex = 0; // TODO: Use shuffle order to get first index.
      newPositionMs = C.TIME_UNSET;
    }
    if (!newTimeline.isEmpty() && newPositionMs == C.TIME_UNSET) {
      newPositionMs = newTimeline.getWindow(newIndex, window).getDefaultPositionMs();
    }
    boolean oldOrNewPlaylistEmpty = oldState.timeline.isEmpty() || newTimeline.isEmpty();
    boolean mediaItemChanged =
        !oldOrNewPlaylistEmpty
            && !oldState
                .timeline
                .getWindow(getCurrentMediaItemIndexInternal(oldState), window)
                .uid
                .equals(newTimeline.getWindow(newIndex, window).uid);
    // Set timeline, resolving tracks and metadata to the new index.
    if (newTimeline.isEmpty()) {
      stateBuilder.setPlaylist(newTimeline, Tracks.EMPTY, /* currentMetadata= */ null);
    } else if (newTimeline instanceof PlaylistTimeline) {
      MediaItemData mediaItemData = ((PlaylistTimeline) newTimeline).playlist.get(newIndex);
      stateBuilder.setPlaylist(newTimeline, mediaItemData.tracks, mediaItemData.mediaMetadata);
    } else {
      boolean keepTracksAndMetadata = !oldOrNewPlaylistEmpty && !mediaItemChanged;
      stateBuilder.setPlaylist(
          newTimeline,
          keepTracksAndMetadata ? oldState.currentTracks : Tracks.EMPTY,
          keepTracksAndMetadata ? oldState.currentMetadata : null);
    }
    if (oldOrNewPlaylistEmpty || mediaItemChanged || newPositionMs < oldPositionMs) {
      // New item or seeking back. Assume no buffer and no ad playback persists.
      stateBuilder
          .setCurrentMediaItemIndex(newIndex)
          .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET)
          .setContentPositionMs(newPositionMs)
          .setContentBufferedPositionMs(PositionSupplier.getConstant(newPositionMs))
          .setTotalBufferedDurationMs(PositionSupplier.ZERO);
    } else if (newPositionMs == oldPositionMs) {
      // Unchanged position. Assume ad playback and buffer in current item persists.
      stateBuilder.setCurrentMediaItemIndex(newIndex);
      if (oldState.currentAdGroupIndex != C.INDEX_UNSET && keepAds) {
        stateBuilder.setTotalBufferedDurationMs(
            PositionSupplier.getConstant(
                oldState.adBufferedPositionMsSupplier.get() - oldState.adPositionMsSupplier.get()));
      } else {
        stateBuilder
            .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET)
            .setTotalBufferedDurationMs(
                PositionSupplier.getConstant(
                    getContentBufferedPositionMsInternal(oldState, window) - oldPositionMs));
      }
    } else {
      // Seeking forward. Assume remaining buffer in current item persist, but no ad playback.
      long contentBufferedDurationMs =
          max(getContentBufferedPositionMsInternal(oldState, window), newPositionMs);
      long totalBufferedDurationMs =
          max(0, oldState.totalBufferedDurationMsSupplier.get() - (newPositionMs - oldPositionMs));
      stateBuilder
          .setCurrentMediaItemIndex(newIndex)
          .setCurrentAd(C.INDEX_UNSET, C.INDEX_UNSET)
          .setContentPositionMs(newPositionMs)
          .setContentBufferedPositionMs(PositionSupplier.getConstant(contentBufferedDurationMs))
          .setTotalBufferedDurationMs(PositionSupplier.getConstant(totalBufferedDurationMs));
    }
    return stateBuilder.build();
  }

  private static MediaMetadata getCombinedMediaMetadata(MediaItem mediaItem, Tracks tracks) {
    MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
    int trackGroupCount = tracks.getGroups().size();
    for (int i = 0; i < trackGroupCount; i++) {
      Tracks.Group group = tracks.getGroups().get(i);
      for (int j = 0; j < group.length; j++) {
        if (group.isTrackSelected(j)) {
          Format format = group.getTrackFormat(j);
          if (format.metadata != null) {
            for (int k = 0; k < format.metadata.length(); k++) {
              format.metadata.get(k).populateMediaMetadata(metadataBuilder);
            }
          }
        }
      }
    }
    return metadataBuilder.populate(mediaItem.mediaMetadata).build();
  }

  private static List<MediaItemData> buildMutablePlaylistFromState(
      State state, Timeline.Period period, Timeline.Window window) {
    if (state.timeline instanceof PlaylistTimeline) {
      return new ArrayList<>(((PlaylistTimeline) state.timeline).playlist);
    }
    ArrayList<MediaItemData> items = new ArrayList<>(state.timeline.getWindowCount());
    for (int i = 0; i < state.timeline.getWindowCount(); i++) {
      items.add(MediaItemData.buildFromState(state, /* mediaItemIndex= */ i, period, window));
    }
    return items;
  }

  private static final class PlaceholderUid {}
}