/*
* 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 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;
private ImmutableList<MediaItemData> playlist;
private Timeline timeline;
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;
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.playlist = state.playlist;
this.timeline = state.timeline;
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 list of {@link MediaItemData media items} in the playlist.
*
* <p>All items must have unique {@linkplain MediaItemData.Builder#setUid UIDs}.
*
* @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);
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 media items in the playlist. */
public final ImmutableList<MediaItemData> playlist;
/** The {@link Timeline} derived from the {@link #playlist}. */
public final Timeline timeline;
/** 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) {
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");
} 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.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.playlist = builder.playlist;
this.timeline = builder.timeline;
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);
}
@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
&& Util.areEqual(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)
&& playlist.equals(state.playlist)
&& 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 + playlist.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(ImmutableList<MediaItemData> playlist) {
int mediaItemCount = playlist.size();
this.playlist = 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},
* is 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 final MediaMetadata combinedMediaMetadata;
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;
}
}
combinedMediaMetadata =
mediaMetadata != null ? mediaMetadata : getCombinedMediaMetadata(mediaItem, tracks);
}
/** 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 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);
}
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();
}
}
/** 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);
});
}
@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.playlist.size();
if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS) || mediaItems.isEmpty()) {
return;
}
int correctedIndex = min(index, playlistSize);
updateStateForPendingOperation(
/* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems),
/* placeholderStateSupplier= */ () -> {
ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>(state.playlist);
for (int i = 0; i < mediaItems.size(); i++) {
placeholderPlaylist.add(
i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
}
return getStateWithNewPlaylist(state, placeholderPlaylist, period);
});
}
@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.playlist.size();
if (!shouldHandleCommand(Player.COMMAND_CHANGE_MEDIA_ITEMS)
|| playlistSize == 0
|| fromIndex >= playlistSize) {
return;
}
int correctedToIndex = min(toIndex, playlistSize);
int correctedNewIndex = min(newIndex, state.playlist.size() - (correctedToIndex - fromIndex));
if (fromIndex == correctedToIndex || correctedNewIndex == fromIndex) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleMoveMediaItems(
fromIndex, correctedToIndex, correctedNewIndex),
/* placeholderStateSupplier= */ () -> {
ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>(state.playlist);
Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex);
return getStateWithNewPlaylist(state, placeholderPlaylist, period);
});
}
@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.playlist.size();
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= */ () -> {
ArrayList<MediaItemData> placeholderPlaylist = new ArrayList<>(state.playlist);
Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
return getStateWithNewPlaylist(state, placeholderPlaylist, period);
});
}
@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 >= 0);
// Use a local copy to ensure the lambda below uses the current state value.
State state = this.state;
if (!shouldHandleCommand(seekCommand)
|| isPlayingAd()
|| (!state.playlist.isEmpty() && mediaItemIndex >= state.playlist.size())) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleSeek(mediaItemIndex, positionMs, seekCommand),
/* placeholderStateSupplier= */ () ->
getStateWithNewPlaylistAndPosition(state, state.playlist, mediaItemIndex, positionMs),
/* seeked= */ true,
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)))
.setAdBufferedPositionMs(state.adPositionMsSupplier)
.setIsLoading(false)
.build());
}
@Override
public final void stop(boolean reset) {
stop();
if (reset) {
clearMediaItems();
}
}
@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 (released) { // TODO(b/261158047): Replace by !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)))
.setAdBufferedPositionMs(state.adPositionMsSupplier)
.setIsLoading(false)
.build();
}
@Override
public final Tracks getCurrentTracks() {
verifyApplicationThreadAndInitState();
return getCurrentTracksInternal(state);
}
@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 getMediaMetadataInternal(state);
}
@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_MEDIA_ITEMS_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);
}
@Override
public final long getContentBufferedPosition() {
verifyApplicationThreadAndInitState();
return max(getContentBufferedPositionMsInternal(state), getContentPositionMsInternal(state));
}
@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;
}
@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),
/* placeholderStateSupplier= */ () -> state.buildUpon().setDeviceVolume(volume).build());
}
@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(),
/* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(state.deviceVolume + 1).build());
}
@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(),
/* placeholderStateSupplier= */ () ->
state.buildUpon().setDeviceVolume(max(0, state.deviceVolume - 1)).build());
}
@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),
/* placeholderStateSupplier= */ () -> state.buildUpon().setIsDeviceMuted(muted).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(), /* seeked= */ 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}.
*
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
// TODO(b/261158047): Add that this method will only be called if COMMAND_RELEASE is available.
@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_MEDIA_ITEMS_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_MEDIA_ITEMS_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}.
*
* <p>Will only be called if {@link Player#COMMAND_SET_DEVICE_VOLUME} is available.
*
* @param deviceVolume The requested device volume.
* @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) {
throw new IllegalStateException("Missing implementation to handle COMMAND_SET_DEVICE_VOLUME");
}
/**
* Handles calls to {@link Player#increaseDeviceVolume()}.
*
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available.
*
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleIncreaseDeviceVolume() {
throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME");
}
/**
* Handles calls to {@link Player#decreaseDeviceVolume()}.
*
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available.
*
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleDecreaseDeviceVolume() {
throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME");
}
/**
* Handles calls to {@link Player#setDeviceMuted}.
*
* <p>Will only be called if {@link Player#COMMAND_ADJUST_DEVICE_VOLUME} is available.
*
* @param muted Whether the device was requested to be muted.
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
*/
@ForOverride
protected ListenableFuture<?> handleSetDeviceMuted(boolean muted) {
throw new IllegalStateException(
"Missing implementation to handle COMMAND_ADJUST_DEVICE_VOLUME");
}
/**
* 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 <= {@code
* index} <= {@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 <= {@code
* fromIndex} < {@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} < {@code toIndex} <= {@link
* #getMediaItemCount()}.
* @param newIndex The new index of the first moved item. The index is in the range {@code 0}
* <= {@code newIndex} < {@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#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
* <= {@code fromIndex} < {@link #getMediaItemCount()}.
* @param toIndex The index of the first item to be kept (exclusive). The index is in the range
* {@code fromIndex} < {@code toIndex} <= {@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. The index is in the range 0 <= {@code
* mediaItemIndex} < {@code mediaItems.size()}.
* @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.
* @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_*");
}
@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 seeked, 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;
Tracks previousTracks = getCurrentTracksInternal(previousState);
Tracks newTracks = getCurrentTracksInternal(newState);
MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState);
MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState);
int positionDiscontinuityReason =
getPositionDiscontinuityReason(previousState, newState, seeked, 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.playlist, newState.playlist);
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.playlist.get(getCurrentMediaItemIndexInternal(newState)).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 (!previousTracks.equals(newTracks)) {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(newTracks));
}
if (!previousMediaMetadata.equals(newMediaMetadata)) {
listeners.queueEvent(
EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(newMediaMetadata));
}
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 (positionDiscontinuityReason == Player.DISCONTINUITY_REASON_SEEK) {
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed);
}
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() {
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"
+ "See https://exoplayer.dev/issues/player-accessed-on-wrong-thread",
Thread.currentThread().getName(), applicationLooper.getThread().getName());
throw new IllegalStateException(message);
}
if (state == null) {
// First time accessing state.
state = getState();
}
}
@RequiresNonNull("state")
private void updateStateForPendingOperation(
ListenableFuture<?> pendingOperation, Supplier<State> placeholderStateSupplier) {
updateStateForPendingOperation(
pendingOperation,
placeholderStateSupplier,
/* seeked= */ false,
/* isRepeatingCurrentItem= */ false);
}
@RequiresNonNull("state")
private void updateStateForPendingOperation(
ListenableFuture<?> pendingOperation,
Supplier<State> placeholderStateSupplier,
boolean seeked,
boolean isRepeatingCurrentItem) {
if (pendingOperation.isDone() && pendingOperations.isEmpty()) {
updateStateAndInformListeners(getState(), seeked, isRepeatingCurrentItem);
} else {
pendingOperations.add(pendingOperation);
State suggestedPlaceholderState = placeholderStateSupplier.get();
updateStateAndInformListeners(
getPlaceholderState(suggestedPlaceholderState), seeked, isRepeatingCurrentItem);
pendingOperation.addListener(
() -> {
castNonNull(state); // Already checked by method @RequiresNonNull pre-condition.
pendingOperations.remove(pendingOperation);
if (pendingOperations.isEmpty() && !released) {
updateStateAndInformListeners(
getState(), /* seeked= */ 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 Tracks getCurrentTracksInternal(State state) {
return state.playlist.isEmpty()
? Tracks.EMPTY
: state.playlist.get(getCurrentMediaItemIndexInternal(state)).tracks;
}
private static MediaMetadata getMediaMetadataInternal(State state) {
return state.playlist.isEmpty()
? MediaMetadata.EMPTY
: state.playlist.get(getCurrentMediaItemIndexInternal(state)).combinedMediaMetadata;
}
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) {
return getPositionOrDefaultInMediaItem(state.contentPositionMsSupplier.get(), state);
}
private static long getContentBufferedPositionMsInternal(State state) {
return getPositionOrDefaultInMediaItem(state.contentBufferedPositionMsSupplier.get(), state);
}
private static long getPositionOrDefaultInMediaItem(long positionMs, State state) {
if (positionMs != C.TIME_UNSET) {
return positionMs;
}
if (state.playlist.isEmpty()) {
return 0;
}
return usToMs(state.playlist.get(getCurrentMediaItemIndexInternal(state)).defaultPositionUs);
}
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, 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(
List<MediaItemData> previousPlaylist, List<MediaItemData> newPlaylist) {
if (previousPlaylist.size() != newPlaylist.size()) {
return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
}
for (int i = 0; i < previousPlaylist.size(); i++) {
Object previousUid = previousPlaylist.get(i).uid;
Object newUid = newPlaylist.get(i).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 seeked,
Timeline.Window window,
Timeline.Period period) {
if (newState.hasPositionDiscontinuity) {
// We were asked to report a discontinuity.
return newState.positionDiscontinuityReason;
}
if (seeked) {
return Player.DISCONTINUITY_REASON_SEEK;
}
if (previousState.playlist.isEmpty()) {
// First change from an empty playlist is not reported as a discontinuity.
return C.INDEX_UNSET;
}
if (newState.playlist.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);
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);
long newPositionMs = getCurrentPeriodOrAdPositionMs(newState, newPeriodUid, period);
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) {
return state.currentAdGroupIndex != C.INDEX_UNSET
? state.adPositionMsSupplier.get()
: getContentPositionMsInternal(state)
- 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);
} else {
contentPositionMs = getContentPositionMsInternal(state);
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
&& getContentPositionMsInternal(previousState) > getContentPositionMsInternal(newState)) {
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(
List<MediaItemData> oldPlaylist,
Timeline newPlaylistTimeline,
int oldMediaItemIndex,
Timeline.Period period) {
if (oldPlaylist.isEmpty()) {
return oldMediaItemIndex < newPlaylistTimeline.getWindowCount()
? oldMediaItemIndex
: C.INDEX_UNSET;
}
Object oldFirstPeriodUid =
oldPlaylist.get(oldMediaItemIndex).getPeriodUid(/* periodIndexInMediaItem= */ 0);
if (newPlaylistTimeline.getIndexOfPeriod(oldFirstPeriodUid) == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
return newPlaylistTimeline.getPeriodByUid(oldFirstPeriodUid, period).windowIndex;
}
private static State getStateWithNewPlaylist(
State oldState, List<MediaItemData> newPlaylist, Timeline.Period period) {
State.Builder stateBuilder = oldState.buildUpon();
stateBuilder.setPlaylist(newPlaylist);
Timeline newTimeline = stateBuilder.timeline;
long oldPositionMs = oldState.contentPositionMsSupplier.get();
int oldIndex = getCurrentMediaItemIndexInternal(oldState);
int newIndex = getMediaItemIndexInNewPlaylist(oldState.playlist, newTimeline, oldIndex, period);
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 < oldState.playlist.size(); i++) {
// TODO: Use shuffle order to iterate.
newIndex =
getMediaItemIndexInNewPlaylist(
oldState.playlist, newTimeline, /* oldMediaItemIndex= */ i, period);
}
// 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,
newPlaylist,
newIndex,
newPositionMs,
/* keepAds= */ true);
}
private static State getStateWithNewPlaylistAndPosition(
State oldState, List<MediaItemData> newPlaylist, int newIndex, long newPositionMs) {
State.Builder stateBuilder = oldState.buildUpon();
stateBuilder.setPlaylist(newPlaylist);
if (oldState.playbackState != Player.STATE_IDLE) {
if (newPlaylist.isEmpty()) {
stateBuilder.setPlaybackState(Player.STATE_ENDED).setIsLoading(false);
} else {
stateBuilder.setPlaybackState(Player.STATE_BUFFERING);
}
}
long oldPositionMs = oldState.contentPositionMsSupplier.get();
return buildStateForNewPosition(
stateBuilder,
oldState,
oldPositionMs,
newPlaylist,
newIndex,
newPositionMs,
/* keepAds= */ false);
}
private static State buildStateForNewPosition(
State.Builder stateBuilder,
State oldState,
long oldPositionMs,
List<MediaItemData> newPlaylist,
int newIndex,
long newPositionMs,
boolean keepAds) {
// Resolve unset or invalid index and position.
oldPositionMs = getPositionOrDefaultInMediaItem(oldPositionMs, oldState);
if (!newPlaylist.isEmpty() && (newIndex == C.INDEX_UNSET || newIndex >= newPlaylist.size())) {
newIndex = 0; // TODO: Use shuffle order to get first index.
newPositionMs = C.TIME_UNSET;
}
if (!newPlaylist.isEmpty() && newPositionMs == C.TIME_UNSET) {
newPositionMs = usToMs(newPlaylist.get(newIndex).defaultPositionUs);
}
boolean oldOrNewPlaylistEmpty = oldState.playlist.isEmpty() || newPlaylist.isEmpty();
boolean mediaItemChanged =
!oldOrNewPlaylistEmpty
&& !oldState
.playlist
.get(getCurrentMediaItemIndexInternal(oldState))
.uid
.equals(newPlaylist.get(newIndex).uid);
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) - oldPositionMs));
}
} else {
// Seeking forward. Assume remaining buffer in current item persist, but no ad playback.
long contentBufferedDurationMs =
max(getContentBufferedPositionMsInternal(oldState), 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 final class PlaceholderUid {}
}