From b81cd08271cd94f244ca5d7c995cd29523057a60 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 18 Nov 2022 18:10:57 +0000 Subject: [PATCH] Add remaining state and getters to SimpleBasePlayer This adds the full Builders and State representation needed to implement all Player getter methods and listener invocations. PiperOrigin-RevId: 489503319 --- .../android/exoplayer2/SimpleBasePlayer.java | 2364 ++++++++++++++++- .../exoplayer2/SimpleBasePlayerTest.java | 1576 ++++++++++- 2 files changed, 3829 insertions(+), 111 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java index 24372623157..a962b8220bb 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/SimpleBasePlayer.java @@ -15,14 +15,21 @@ */ package com.google.android.exoplayer2; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Util.castNonNull; +import static com.google.android.exoplayer2.util.Util.usToMs; +import static java.lang.Math.max; 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 com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.text.CueGroup; @@ -34,10 +41,12 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoSize; 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.HashMap; import java.util.HashSet; import java.util.List; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; @@ -91,18 +100,138 @@ 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 int audioSessionId; + private boolean skipSilenceEnabled; + private Size surfaceSize; + private boolean newlyRenderedFirstFrame; + private Metadata timedMetadata; + private ImmutableList playlistItems; + private Timeline timeline; + private MediaMetadata playlistMetadata; + private int currentMediaItemIndex; + private int currentPeriodIndex; + private int currentAdGroupIndex; + private int currentAdIndexInAdGroup; + private long contentPositionMs; + private PositionSupplier contentPositionMsSupplier; + 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; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + skipSilenceEnabled = false; + surfaceSize = Size.UNKNOWN; + newlyRenderedFirstFrame = false; + timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); + playlistItems = ImmutableList.of(); + timeline = Timeline.EMPTY; + playlistMetadata = MediaMetadata.EMPTY; + currentMediaItemIndex = 0; + currentPeriodIndex = C.INDEX_UNSET; + currentAdGroupIndex = C.INDEX_UNSET; + currentAdIndexInAdGroup = C.INDEX_UNSET; + contentPositionMs = C.TIME_UNSET; + contentPositionMsSupplier = PositionSupplier.ZERO; + adPositionMs = C.TIME_UNSET; + adPositionMsSupplier = PositionSupplier.ZERO; + contentBufferedPositionMsSupplier = PositionSupplier.ZERO; + 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.audioSessionId = state.audioSessionId; + this.skipSilenceEnabled = state.skipSilenceEnabled; + this.surfaceSize = state.surfaceSize; + this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; + this.timedMetadata = state.timedMetadata; + this.playlistItems = state.playlistItems; + this.timeline = state.timeline; + this.playlistMetadata = state.playlistMetadata; + this.currentMediaItemIndex = state.currentMediaItemIndex; + this.currentPeriodIndex = state.currentPeriodIndex; + this.currentAdGroupIndex = state.currentAdGroupIndex; + this.currentAdIndexInAdGroup = state.currentAdIndexInAdGroup; + this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMsSupplier = state.contentPositionMsSupplier; + this.adPositionMs = C.TIME_UNSET; + 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; } /** @@ -133,26 +262,1646 @@ public Builder setPlayWhenReady( return this; } + /** + * Sets the {@linkplain Player.State state} of the player. + * + *

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. + * + *

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. + * + *

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 audio session id. + * + * @param audioSessionId The audio session id. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAudioSessionId(int audioSessionId) { + this.audioSessionId = audioSessionId; + return this; + } + + /** + * Sets whether skipping silences in the audio stream is enabled. + * + * @param skipSilenceEnabled Whether skipping silences in the audio stream is enabled. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setSkipSilenceEnabled(boolean skipSilenceEnabled) { + this.skipSilenceEnabled = skipSilenceEnabled; + 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. + * + *

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}. + * + *

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

All playlist items must have unique {@linkplain PlaylistItem.Builder#setUid UIDs}. + * + * @param playlistItems The list of playlist items. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaylist(List playlistItems) { + HashSet uids = new HashSet<>(); + for (int i = 0; i < playlistItems.size(); i++) { + checkArgument(uids.add(playlistItems.get(i).uid)); + } + this.playlistItems = ImmutableList.copyOf(playlistItems); + this.timeline = new PlaylistTimeline(this.playlistItems); + 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. + * + *

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

The period index must be less than the total number of {@linkplain + * PlaylistItem.Builder#setPeriods periods} in the playlist, if set, and the period at the + * specified index must be part of the {@linkplain #setCurrentMediaItemIndex current playlist + * item}. + * + * @param currentPeriodIndex The current period index, or {@link C#INDEX_UNSET} to assume the + * first period of the current playlist item is played. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setCurrentPeriodIndex(int currentPeriodIndex) { + checkArgument(currentPeriodIndex == C.INDEX_UNSET || currentPeriodIndex >= 0); + this.currentPeriodIndex = currentPeriodIndex; + return this; + } + + /** + * Sets the current ad indices, or {@link C#INDEX_UNSET} if no ad is playing. + * + *

Either both indices need to be {@link C#INDEX_UNSET} or both are not {@link + * C#INDEX_UNSET}. + * + *

Ads indices can only be set if there is a corresponding {@link AdPlaybackState} defined + * in the current {@linkplain PlaylistItem.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. + * + *

This position will be converted to an advancing {@link PositionSupplier} if the overall + * state indicates an advancing playback position. + * + * @param positionMs The current content playback position in milliseconds. + * @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. + * + *

The supplier is expected to return the updated position on every call if the playback is + * advancing, for example by using {@link PositionSupplier#getExtrapolating}. + * + * @param contentPositionMsSupplier The {@link PositionSupplier} for the current content + * playback position in milliseconds. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setContentPositionMs(PositionSupplier contentPositionMsSupplier) { + this.contentPositionMs = C.TIME_UNSET; + this.contentPositionMsSupplier = contentPositionMsSupplier; + return this; + } + + /** + * Sets the current ad playback position in milliseconds. The * value is unused if no ad is + * playing. + * + *

This position will be converted to an advancing {@link PositionSupplier} if the overall + * state indicates an advancing ad playback position. + * + * @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. + * + *

The supplier is expected to return the updated position on every call if the playback is + * advancing, for example by using {@link PositionSupplier#getExtrapolating}. + * + * @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 = C.TIME_UNSET; + 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. + * @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 audio session id. */ + public final int audioSessionId; + /** Whether skipping silences in the audio stream is enabled. */ + public final boolean skipSilenceEnabled; + /** 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 playlist items. */ + public final ImmutableList playlistItems; + /** The {@link Timeline} derived from the {@linkplain #playlistItems playlist items}. */ + public final Timeline timeline; + /** The playlist {@link MediaMetadata}. */ + public final MediaMetadata playlistMetadata; + /** The current media item index. */ + public final int currentMediaItemIndex; + /** + * The current period index, or {@link C#INDEX_UNSET} to assume the first period of the current + * playlist item is played. + */ + public final int currentPeriodIndex; + /** 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. */ + 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. + */ + 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); + } else { + checkArgument(builder.currentMediaItemIndex < builder.timeline.getWindowCount()); + if (builder.currentPeriodIndex != C.INDEX_UNSET) { + checkArgument(builder.currentPeriodIndex < builder.timeline.getPeriodCount()); + checkArgument( + builder.timeline.getPeriod(builder.currentPeriodIndex, new Timeline.Period()) + .windowIndex + == builder.currentMediaItemIndex); + } + if (builder.currentAdGroupIndex != C.INDEX_UNSET) { + int periodIndex = + builder.currentPeriodIndex != C.INDEX_UNSET + ? builder.currentPeriodIndex + : builder.timeline.getWindow(builder.currentMediaItemIndex, new Timeline.Window()) + .firstPeriodIndex; + Timeline.Period period = builder.timeline.getPeriod(periodIndex, new Timeline.Period()); + checkArgument(builder.currentAdGroupIndex < period.getAdGroupCount()); + int adCountInGroup = period.getAdCountInAdGroup(builder.currentAdGroupIndex); + if (adCountInGroup != C.LENGTH_UNSET) { + checkArgument(builder.currentAdIndexInAdGroup < adCountInGroup); + } + } + } + if (builder.playerError != null) { + checkArgument(builder.playbackState == Player.STATE_IDLE); + } + if (builder.playbackState == Player.STATE_IDLE + || builder.playbackState == Player.STATE_ENDED) { + checkArgument(!builder.isLoading); + } + PositionSupplier contentPositionMsSupplier = builder.contentPositionMsSupplier; + if (builder.contentPositionMs != C.TIME_UNSET) { + if (builder.currentAdGroupIndex == C.INDEX_UNSET + && builder.playWhenReady + && builder.playbackState == Player.STATE_READY + && builder.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE) { + contentPositionMsSupplier = + PositionSupplier.getExtrapolating( + builder.contentPositionMs, builder.playbackParameters.speed); + } else { + contentPositionMsSupplier = PositionSupplier.getConstant(builder.contentPositionMs); + } + } + PositionSupplier adPositionMsSupplier = builder.adPositionMsSupplier; + if (builder.adPositionMs != C.TIME_UNSET) { + 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.audioSessionId = builder.audioSessionId; + this.skipSilenceEnabled = builder.skipSilenceEnabled; + this.surfaceSize = builder.surfaceSize; + this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; + this.timedMetadata = builder.timedMetadata; + this.playlistItems = builder.playlistItems; + this.timeline = builder.timeline; + this.playlistMetadata = builder.playlistMetadata; + this.currentMediaItemIndex = builder.currentMediaItemIndex; + this.currentPeriodIndex = builder.currentPeriodIndex; + 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 + && audioSessionId == state.audioSessionId + && skipSilenceEnabled == state.skipSilenceEnabled + && surfaceSize.equals(state.surfaceSize) + && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame + && timedMetadata.equals(state.timedMetadata) + && playlistItems.equals(state.playlistItems) + && playlistMetadata.equals(state.playlistMetadata) + && currentMediaItemIndex == state.currentMediaItemIndex + && currentPeriodIndex == state.currentPeriodIndex + && 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 + audioSessionId; + result = 31 * result + (skipSilenceEnabled ? 1 : 0); + result = 31 * result + surfaceSize.hashCode(); + result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); + result = 31 * result + timedMetadata.hashCode(); + result = 31 * result + playlistItems.hashCode(); + result = 31 * result + playlistMetadata.hashCode(); + result = 31 * result + currentMediaItemIndex; + result = 31 * result + currentPeriodIndex; + 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 playlistItems; + private final int[] firstPeriodIndexByWindowIndex; + private final int[] windowIndexByPeriodIndex; + private final HashMap periodIndexByUid; + + public PlaylistTimeline(ImmutableList playlistItems) { + int playlistItemCount = playlistItems.size(); + this.playlistItems = playlistItems; + this.firstPeriodIndexByWindowIndex = new int[playlistItemCount]; + int periodCount = 0; + for (int i = 0; i < playlistItemCount; i++) { + PlaylistItem playlistItem = playlistItems.get(i); + firstPeriodIndexByWindowIndex[i] = periodCount; + periodCount += getPeriodCountInPlaylistItem(playlistItem); + } + this.windowIndexByPeriodIndex = new int[periodCount]; + this.periodIndexByUid = new HashMap<>(); + int periodIndex = 0; + for (int i = 0; i < playlistItemCount; i++) { + PlaylistItem playlistItem = playlistItems.get(i); + for (int j = 0; j < getPeriodCountInPlaylistItem(playlistItem); j++) { + periodIndexByUid.put(playlistItem.getPeriodUid(j), periodIndex); + windowIndexByPeriodIndex[periodIndex] = i; + periodIndex++; + } + } + } + + @Override + public int getWindowCount() { + return playlistItems.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 playlistItems + .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 playlistItems.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 playlistItems.get(windowIndex).getPeriodUid(periodIndexInWindow); + } + + private static int getPeriodCountInPlaylistItem(PlaylistItem playlistItem) { + return playlistItem.periods.isEmpty() ? 1 : playlistItem.periods.size(); + } + } + + /** + * An immutable description of a playlist item, 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 PlaylistItem { + + /** A builder for {@link PlaylistItem} 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 periods; + + /** + * Creates the builder. + * + * @param uid The unique identifier of the playlist 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(PlaylistItem playlistItem) { + this.uid = playlistItem.uid; + this.tracks = playlistItem.tracks; + this.mediaItem = playlistItem.mediaItem; + this.mediaMetadata = playlistItem.mediaMetadata; + this.manifest = playlistItem.manifest; + this.liveConfiguration = playlistItem.liveConfiguration; + this.presentationStartTimeMs = playlistItem.presentationStartTimeMs; + this.windowStartTimeMs = playlistItem.windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = playlistItem.elapsedRealtimeEpochOffsetMs; + this.isSeekable = playlistItem.isSeekable; + this.isDynamic = playlistItem.isDynamic; + this.defaultPositionUs = playlistItem.defaultPositionUs; + this.durationUs = playlistItem.durationUs; + this.positionInFirstPeriodUs = playlistItem.positionInFirstPeriodUs; + this.isPlaceholder = playlistItem.isPlaceholder; + this.periods = playlistItem.periods; + } + + /** + * Sets the unique identifier of this playlist item within a playlist. + * + *

This value will be set as {@link Timeline.Window#uid} for this item. + * + * @param uid The unique identifier of this playlist item within a playlist. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setUid(Object uid) { + this.uid = uid; + return this; + } + + /** + * Sets the {@link Tracks} of this playlist item. + * + * @param tracks The {@link Tracks} of this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setTracks(Tracks tracks) { + this.tracks = tracks; + return this; + } + + /** + * Sets the {@link MediaItem} for this playlist item. + * + * @param mediaItem The {@link MediaItem} for this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setMediaItem(MediaItem mediaItem) { + this.mediaItem = mediaItem; + return this; + } + + /** + * Sets the {@link MediaMetadata}. + * + *

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 playlist item. + * + * @param manifest The manifest of the playlist 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 playlist item is not + * live. + * + * @param liveConfiguration The active {@link MediaItem.LiveConfiguration}, or null if the + * playlist 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. + * + *

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. + * + *

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. + * + *

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 playlist item. + * + * @param isSeekable Whether it's possible to seek within this playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setIsSeekable(boolean isSeekable) { + this.isSeekable = isSeekable; + return this; + } + + /** + * Sets whether this playlist item may change over time, for example a moving live window. + * + * @param isDynamic Whether this playlist 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 playlist item at which to begin + * playback, in microseconds. + * + *

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 playlist 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 playlist item, in microseconds. + * + *

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 playlist 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 playlist item relative to the start of the first + * period belonging to it, in microseconds. + * + * @param positionInFirstPeriodUs The position of the start of this playlist 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 playlist item contains placeholder information because the real + * information has yet to be loaded. + * + * @param isPlaceholder Whether this playlist 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 playlist item. + * + *

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 playlist item, or an + * empty list to assume a single period without ads and the same duration as the playlist + * item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPeriods(List periods) { + int periodCount = periods.size(); + for (int i = 0; i < periodCount - 1; i++) { + checkArgument(periods.get(i).durationUs != C.TIME_UNSET); + for (int j = i + 1; j < periodCount; j++) { + checkArgument(!periods.get(i).uid.equals(periods.get(j).uid)); + } + } + this.periods = ImmutableList.copyOf(periods); + return this; + } + + /** Builds the {@link PlaylistItem}. */ + public PlaylistItem build() { + return new PlaylistItem(this); + } + } + + /** The unique identifier of this playlist item. */ + public final Object uid; + /** The {@link Tracks} of this playlist item. */ + public final Tracks tracks; + /** The {@link MediaItem} for this playlist item. */ + 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 playlist item, or null if not applicable. */ + @Nullable public final Object manifest; + /** The active {@link MediaItem.LiveConfiguration}, or null if the playlist 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 playlist item. */ + public final boolean isSeekable; + /** Whether this playlist item may change over time, for example a moving live window. */ + public final boolean isDynamic; + /** + * The default position relative to the start of the playlist item at which to begin playback, + * in microseconds. + */ + public final long defaultPositionUs; + /** The duration of the playlist item, in microseconds, or {@link C#TIME_UNSET} if unknown. */ + public final long durationUs; + /** + * The position of the start of this playlist item relative to the start of the first period + * belonging to it, in microseconds. + */ + public final long positionInFirstPeriodUs; + /** + * Whether this playlist 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 playlist item, or an empty list to assume + * a single period without ads and the same duration as the playlist item. + */ + public final ImmutableList periods; + + private final long[] periodPositionInWindowUs; + private final MediaMetadata combinedMediaMetadata; + + private PlaylistItem(Builder builder) { + if (builder.liveConfiguration == null) { + checkArgument(builder.presentationStartTimeMs == C.TIME_UNSET); + checkArgument(builder.windowStartTimeMs == C.TIME_UNSET); + checkArgument(builder.elapsedRealtimeEpochOffsetMs == C.TIME_UNSET); + } else if (builder.presentationStartTimeMs != C.TIME_UNSET + && builder.windowStartTimeMs != C.TIME_UNSET) { + checkArgument(builder.windowStartTimeMs >= builder.presentationStartTimeMs); + } + int periodCount = builder.periods.size(); + if (builder.durationUs != C.TIME_UNSET) { + checkArgument(builder.defaultPositionUs <= builder.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 PlaylistItem)) { + return false; + } + PlaylistItem playlistItem = (PlaylistItem) o; + return this.uid.equals(playlistItem.uid) + && this.tracks.equals(playlistItem.tracks) + && this.mediaItem.equals(playlistItem.mediaItem) + && Util.areEqual(this.mediaMetadata, playlistItem.mediaMetadata) + && Util.areEqual(this.manifest, playlistItem.manifest) + && Util.areEqual(this.liveConfiguration, playlistItem.liveConfiguration) + && this.presentationStartTimeMs == playlistItem.presentationStartTimeMs + && this.windowStartTimeMs == playlistItem.windowStartTimeMs + && this.elapsedRealtimeEpochOffsetMs == playlistItem.elapsedRealtimeEpochOffsetMs + && this.isSeekable == playlistItem.isSeekable + && this.isDynamic == playlistItem.isDynamic + && this.defaultPositionUs == playlistItem.defaultPositionUs + && this.durationUs == playlistItem.durationUs + && this.positionInFirstPeriodUs == playlistItem.positionInFirstPeriodUs + && this.isPlaceholder == playlistItem.isPlaceholder + && this.periods.equals(playlistItem.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 periodIndexInPlaylistItem, 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(periodIndexInPlaylistItem); + Object periodId = periodData.uid; + Object periodUid = Pair.create(uid, periodId); + period.set( + periodId, + periodUid, + windowIndex, + periodData.durationUs, + periodPositionInWindowUs[periodIndexInPlaylistItem], + periodData.adPlaybackState, + periodData.isPlaceholder); + } + return period; + } + + private Object getPeriodUid(int periodIndexInPlaylistItem) { + if (periods.isEmpty()) { + return uid; + } + Object periodId = periods.get(periodIndexInPlaylistItem).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 PlaylistItem}. */ + 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 playlist 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 playlist item. + * + * @param uid The unique identifier of the period within its playlist item. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setUid(Object uid) { + this.uid = uid; + return 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; + /** + * Sets the total duration of the period, in microseconds, or {@link C#TIME_UNSET} if unknown. + * + *

Only the last period in a playlist 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; + } - private State(Builder builder) { - this.availableCommands = builder.availableCommands; - this.playWhenReady = builder.playWhenReady; - this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason; + /** + * 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); + } } - /** Returns a {@link Builder} pre-populated with the current state values. */ + /** The unique identifier of the period within its playlist 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 playlist 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); } @@ -162,29 +1911,71 @@ public boolean equals(@Nullable Object o) { if (this == o) { return true; } - if (!(o instanceof State)) { + if (!(o instanceof PeriodData)) { return false; } - State state = (State) o; - return playWhenReady == state.playWhenReady - && playWhenReadyChangeReason == state.playWhenReadyChangeReason - && availableCommands.equals(state.availableCommands); + 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 + availableCommands.hashCode(); - result = 31 * result + (playWhenReady ? 1 : 0); - result = 31 * result + playWhenReadyChangeReason; + 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 listeners; private final Looper applicationLooper; private final HandlerWrapper applicationHandler; private final HashSet> pendingOperations; + private final Timeline.Period period; private @MonotonicNonNull State state; @@ -209,6 +2000,7 @@ 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 listenerSet = new ListenerSet<>( @@ -303,34 +2095,36 @@ public final void prepare() { } @Override + @Player.State public final int getPlaybackState() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackState; } @Override public final int getPlaybackSuppressionReason() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackSuppressionReason; } @Nullable @Override public final PlaybackException getPlayerError() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playerError; } @Override - public final void setRepeatMode(int repeatMode) { + public final void setRepeatMode(@Player.RepeatMode int repeatMode) { // TODO: implement. throw new IllegalStateException(); } @Override + @Player.RepeatMode public final int getRepeatMode() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.repeatMode; } @Override @@ -341,14 +2135,14 @@ public final void setShuffleModeEnabled(boolean shuffleModeEnabled) { @Override public final boolean getShuffleModeEnabled() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.shuffleModeEnabled; } @Override public final boolean isLoading() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.isLoading; } @Override @@ -359,20 +2153,20 @@ public final void seekTo(int mediaItemIndex, long positionMs) { @Override public final long getSeekBackIncrement() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.seekBackIncrementMs; } @Override public final long getSeekForwardIncrement() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.seekForwardIncrementMs; } @Override public final long getMaxSeekToPreviousPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.maxSeekToPreviousPositionMs; } @Override @@ -383,8 +2177,8 @@ public final void setPlaybackParameters(PlaybackParameters playbackParameters) { @Override public final PlaybackParameters getPlaybackParameters() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playbackParameters; } @Override @@ -407,14 +2201,14 @@ public final void release() { @Override public final Tracks getCurrentTracks() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getCurrentTracksInternal(state); } @Override public final TrackSelectionParameters getTrackSelectionParameters() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.trackSelectionParameters; } @Override @@ -425,14 +2219,14 @@ public final void setTrackSelectionParameters(TrackSelectionParameters parameter @Override public final MediaMetadata getMediaMetadata() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getMediaMetadataInternal(state); } @Override public final MediaMetadata getPlaylistMetadata() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.playlistMetadata; } @Override @@ -443,80 +2237,89 @@ public final void setPlaylistMetadata(MediaMetadata mediaMetadata) { @Override public final Timeline getCurrentTimeline() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.timeline; } @Override public final int getCurrentPeriodIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return getCurrentPeriodIndexInternal(state, window); } @Override public final int getCurrentMediaItemIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentMediaItemIndex; } @Override public final long getDuration() { - // TODO: implement. - throw new IllegalStateException(); + 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() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return isPlayingAd() ? state.adPositionMsSupplier.get() : getContentPosition(); } @Override public final long getBufferedPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return isPlayingAd() + ? max(state.adBufferedPositionMsSupplier.get(), state.adPositionMsSupplier.get()) + : getContentBufferedPosition(); } @Override public final long getTotalBufferedDuration() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.totalBufferedDurationMsSupplier.get(); } @Override public final boolean isPlayingAd() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdGroupIndex != C.INDEX_UNSET; } @Override public final int getCurrentAdGroupIndex() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdGroupIndex; } @Override public final int getCurrentAdIndexInAdGroup() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentAdIndexInAdGroup; } @Override public final long getContentPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.contentPositionMsSupplier.get(); } @Override public final long getContentBufferedPosition() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return max( + state.contentBufferedPositionMsSupplier.get(), state.contentPositionMsSupplier.get()); } @Override public final AudioAttributes getAudioAttributes() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.audioAttributes; } @Override @@ -527,8 +2330,8 @@ public final void setVolume(float volume) { @Override public final float getVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.volume; } @Override @@ -587,38 +2390,38 @@ public final void clearVideoTextureView(@Nullable TextureView textureView) { @Override public final VideoSize getVideoSize() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.videoSize; } @Override public final Size getSurfaceSize() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.surfaceSize; } @Override public final CueGroup getCurrentCues() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.currentCues; } @Override public final DeviceInfo getDeviceInfo() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.deviceInfo; } @Override public final int getDeviceVolume() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.deviceVolume; } @Override public final boolean isDeviceMuted() { - // TODO: implement. - throw new IllegalStateException(); + verifyApplicationThreadAndInitState(); + return state.isDeviceMuted; } @Override @@ -721,11 +2524,95 @@ private void updateStateAndInformListeners(State newState) { this.state = newState; boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; - if (playWhenReadyChanged /* TODO: || playbackStateChanged */) { + 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, window, period); + boolean timelineChanged = !previousState.timeline.equals(newState.timeline); + int mediaItemTransitionReason = + getMediaItemTransitionReason(previousState, newState, positionDiscontinuityReason, window); + + if (timelineChanged) { + @Player.TimelineChangeReason + int timelineChangeReason = + getTimelineChangeReason(previousState.playlistItems, newState.playlistItems); + 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= */ state.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 = + state.timeline.isEmpty() + ? null + : state.playlistItems.get(state.currentMediaItemIndex).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, /* TODO */ Player.STATE_IDLE)); + 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) { @@ -735,11 +2622,115 @@ private void updateStateAndInformListeners(State newState) { 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.skipSilenceEnabled != newState.skipSilenceEnabled) { + listeners.queueEvent( + Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, + listener -> listener.onSkipSilenceEnabledChanged(newState.skipSilenceEnabled)); + } + 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 (previousState.audioSessionId != newState.audioSessionId) { + listeners.queueEvent( + Player.EVENT_AUDIO_SESSION_ID, + listener -> listener.onAudioSessionIdChanged(newState.audioSessionId)); + } + 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 (false /* TODO: add flag to know when a seek request has been resolved */) { + listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, Listener::onSeekProcessed); + } if (!previousState.availableCommands.equals(newState.availableCommands)) { listeners.queueEvent( Player.EVENT_AVAILABLE_COMMANDS_CHANGED, @@ -777,7 +2768,7 @@ private void updateStateForPendingOperation( updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState)); pendingOperation.addListener( () -> { - castNonNull(state); // Already check by method @RequiresNonNull pre-condition. + castNonNull(state); // Already checked by method @RequiresNonNull pre-condition. pendingOperations.remove(pendingOperation); if (pendingOperations.isEmpty()) { updateStateAndInformListeners(getState()); @@ -796,8 +2787,191 @@ private void postOrRunOnApplicationHandler(Runnable runnable) { } private static boolean isPlaying(State state) { - return state.playWhenReady && false; - // TODO: && state.playbackState == Player.STATE_READY - // && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE + return state.playWhenReady + && state.playbackState == Player.STATE_READY + && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + } + + private static Tracks getCurrentTracksInternal(State state) { + return state.playlistItems.isEmpty() + ? Tracks.EMPTY + : state.playlistItems.get(state.currentMediaItemIndex).tracks; + } + + private static MediaMetadata getMediaMetadataInternal(State state) { + return state.playlistItems.isEmpty() + ? MediaMetadata.EMPTY + : state.playlistItems.get(state.currentMediaItemIndex).combinedMediaMetadata; + } + + private static int getCurrentPeriodIndexInternal(State state, Timeline.Window window) { + if (state.currentPeriodIndex != C.INDEX_UNSET) { + return state.currentPeriodIndex; + } + if (state.timeline.isEmpty()) { + return state.currentMediaItemIndex; + } + return state.timeline.getWindow(state.currentMediaItemIndex, window).firstPeriodIndex; + } + + private static @Player.TimelineChangeReason int getTimelineChangeReason( + List previousPlaylist, List newPlaylist) { + if (previousPlaylist.size() != newPlaylist.size()) { + return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; + } + for (int i = 0; i < previousPlaylist.size(); i++) { + if (!previousPlaylist.get(i).uid.equals(newPlaylist.get(i).uid)) { + return Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; + } + } + return Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE; + } + + private static int getPositionDiscontinuityReason( + State previousState, State newState, Timeline.Window window, Timeline.Period period) { + if (newState.hasPositionDiscontinuity) { + // We were asked to report a discontinuity. + return newState.positionDiscontinuityReason; + } + if (previousState.playlistItems.isEmpty()) { + // First change from an empty timeline is not reported as a discontinuity. + return C.INDEX_UNSET; + } + if (newState.playlistItems.isEmpty()) { + // The playlist became empty. + return Player.DISCONTINUITY_REASON_REMOVE; + } + Object previousPeriodUid = + previousState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(previousState, window)); + Object newPeriodUid = + newState.timeline.getUidOfPeriod(getCurrentPeriodIndexInternal(newState, window)); + 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() + : state.contentPositionMsSupplier.get() + - 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 = state.currentMediaItemIndex; + int periodIndex = C.INDEX_UNSET; + @Nullable MediaItem mediaItem = null; + if (!state.timeline.isEmpty()) { + periodIndex = getCurrentPeriodIndexInternal(state, window); + 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 + : state.contentPositionMsSupplier.get(); + } else { + contentPositionMs = state.contentPositionMsSupplier.get(); + 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, + 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(previousState.currentMediaItemIndex, window).uid; + Object newWindowUid = newState.timeline.getWindow(newState.currentMediaItemIndex, window).uid; + 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 + && previousState.contentPositionMsSupplier.get() + > newState.contentPositionMsSupplier.get()) { + return MEDIA_ITEM_TRANSITION_REASON_REPEAT; + } + if (positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK + && /* TODO: mark repetition seeks to detect this case */ false) { + return MEDIA_ITEM_TRANSITION_REASON_SEEK; + } + return C.INDEX_UNSET; } } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java b/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java index 7c9fcf1afec..0fb17455b58 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/SimpleBasePlayerTest.java @@ -16,15 +16,28 @@ package com.google.android.exoplayer2; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.os.Looper; +import android.os.SystemClock; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.Commands; import com.google.android.exoplayer2.Player.Listener; import com.google.android.exoplayer2.SimpleBasePlayer.State; +import com.google.android.exoplayer2.testutil.FakeMetadataEntry; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.CueGroup; +import com.google.android.exoplayer2.util.Size; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -35,6 +48,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link SimpleBasePlayer}. */ @RunWith(AndroidJUnit4.class) @@ -61,6 +75,64 @@ public void stateBuildUpon_build_isEqual() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError( + new PlaybackException( + /* message= */ null, + /* cause= */ null, + PlaybackException.ERROR_CODE_DECODING_FAILED)) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT) + .setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build()) + .setVolume(0.5f) + .setVideoSize(new VideoSize(/* width= */ 200, /* height= */ 400)) + .setCurrentCues( + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123)) + .setDeviceInfo( + new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7)) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(new Size(480, 360)) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(new Metadata()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 555, + 666)) + .build())) + .build())) + .setPlaylistMetadata(new MediaMetadata.Builder().setArtist("artist").build()) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .setContentPositionMs(() -> 456) + .setAdPositionMs(() -> 6678) + .setContentBufferedPositionMs(() -> 999) + .setAdBufferedPositionMs(() -> 888) + .setTotalBufferedDurationMs(() -> 567) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 400) .build(); State newState = state.buildUpon().build(); @@ -70,29 +142,622 @@ public void stateBuildUpon_build_isEqual() { } @Test - public void stateBuilderSetAvailableCommands_setsAvailableCommands() { - Commands commands = - new Commands.Builder() - .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + public void playlistItemBuildUpon_build_isEqual() { + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setTracks( + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true})))) + .setMediaItem(new MediaItem.Builder().setMediaId("id").build()) + .setMediaMetadata(new MediaMetadata.Builder().setTitle("title").build()) + .setManifest(new Object()) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build()) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build())) .build(); - State state = new State.Builder().setAvailableCommands(commands).build(); - assertThat(state.availableCommands).isEqualTo(commands); + SimpleBasePlayer.PlaylistItem newPlaylistItem = playlistItem.buildUpon().build(); + + assertThat(newPlaylistItem).isEqualTo(playlistItem); + assertThat(newPlaylistItem.hashCode()).isEqualTo(playlistItem.hashCode()); + } + + @Test + public void periodDataBuildUpon_build_isEqual() { + SimpleBasePlayer.PeriodData periodData = + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build(); + + SimpleBasePlayer.PeriodData newPeriodData = periodData.buildUpon().build(); + + assertThat(newPeriodData).isEqualTo(periodData); + assertThat(newPeriodData.hashCode()).isEqualTo(periodData.hashCode()); } @Test - public void stateBuilderSetPlayWhenReady_setsStatePlayWhenReadyAndReason() { + public void stateBuilderBuild_setsCorrectValues() { + Commands commands = + new Commands.Builder() + .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) + .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + Metadata timedMetadata = new Metadata(new FakeMetadataEntry("data")); + Size surfaceSize = new Size(480, 360); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build())) + .build()); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier adPositionSupplier = () -> 6678; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 999; + SimpleBasePlayer.PositionSupplier adBufferedPositionSupplier = () -> 888; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + State state = new State.Builder() + .setAvailableCommands(commands) .setPlayWhenReady( /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(timedMetadata) + .setPlaylist(playlist) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .setContentPositionMs(contentPositionSupplier) + .setAdPositionMs(adPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setAdBufferedPositionMs(adBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 400) .build(); + assertThat(state.availableCommands).isEqualTo(commands); assertThat(state.playWhenReady).isTrue(); assertThat(state.playWhenReadyChangeReason) .isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS); + assertThat(state.playbackState).isEqualTo(Player.STATE_IDLE); + assertThat(state.playbackSuppressionReason) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(state.playerError).isEqualTo(error); + assertThat(state.repeatMode).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(state.shuffleModeEnabled).isTrue(); + assertThat(state.isLoading).isFalse(); + assertThat(state.seekBackIncrementMs).isEqualTo(5000); + assertThat(state.seekForwardIncrementMs).isEqualTo(4000); + assertThat(state.maxSeekToPreviousPositionMs).isEqualTo(3000); + assertThat(state.playbackParameters).isEqualTo(playbackParameters); + assertThat(state.trackSelectionParameters).isEqualTo(trackSelectionParameters); + assertThat(state.audioAttributes).isEqualTo(audioAttributes); + assertThat(state.volume).isEqualTo(0.5f); + assertThat(state.videoSize).isEqualTo(videoSize); + assertThat(state.currentCues).isEqualTo(cueGroup); + assertThat(state.deviceInfo).isEqualTo(deviceInfo); + assertThat(state.deviceVolume).isEqualTo(5); + assertThat(state.isDeviceMuted).isTrue(); + assertThat(state.audioSessionId).isEqualTo(78); + assertThat(state.skipSilenceEnabled).isTrue(); + assertThat(state.surfaceSize).isEqualTo(surfaceSize); + assertThat(state.newlyRenderedFirstFrame).isTrue(); + assertThat(state.timedMetadata).isEqualTo(timedMetadata); + assertThat(state.playlistItems).isEqualTo(playlist); + assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); + assertThat(state.currentMediaItemIndex).isEqualTo(1); + assertThat(state.currentPeriodIndex).isEqualTo(1); + assertThat(state.currentAdGroupIndex).isEqualTo(1); + assertThat(state.currentAdIndexInAdGroup).isEqualTo(2); + assertThat(state.contentPositionMsSupplier).isEqualTo(contentPositionSupplier); + assertThat(state.adPositionMsSupplier).isEqualTo(adPositionSupplier); + assertThat(state.contentBufferedPositionMsSupplier).isEqualTo(contentBufferedPositionSupplier); + assertThat(state.adBufferedPositionMsSupplier).isEqualTo(adBufferedPositionSupplier); + assertThat(state.totalBufferedDurationMsSupplier).isEqualTo(totalBufferedPositionSupplier); + assertThat(state.hasPositionDiscontinuity).isTrue(); + assertThat(state.positionDiscontinuityReason).isEqualTo(Player.DISCONTINUITY_REASON_SEEK); + assertThat(state.discontinuityPositionMs).isEqualTo(400); + } + + @Test + public void stateBuilderBuild_emptyTimelineWithReadyState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setPlaybackState(Player.STATE_READY) + .build()); + } + + @Test + public void stateBuilderBuild_emptyTimelineWithBufferingState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist(ImmutableList.of()) + .setPlaybackState(Player.STATE_BUFFERING) + .build()); + } + + @Test + public void stateBuilderBuild_idleStateWithIsLoading_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaybackState(Player.STATE_IDLE) + .setIsLoading(true) + .build()); + } + + @Test + public void stateBuilderBuild_currentWindowIndexExceedsPlaylistLength_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentMediaItemIndex(2) + .build()); + } + + @Test + public void stateBuilderBuild_currentPeriodIndexExceedsPlaylistLength_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentPeriodIndex(2) + .build()); + } + + @Test + public void stateBuilderBuild_currentPeriodIndexInOtherMediaItem_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setCurrentMediaItemIndex(0) + .setCurrentPeriodIndex(1) + .build()); + } + + @Test + public void stateBuilderBuild_currentAdGroupIndexExceedsAdGroupCount_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2) + .build()); + } + + @Test + public void stateBuilderBuild_currentAdIndexExceedsAdCountInAdGroup_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount( + /* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2) + .build()); + } + + @Test + public void stateBuilderBuild_playerErrorInNonIdleState_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaybackState(Player.STATE_READY) + .setPlayerError( + new PlaybackException( + /* message= */ null, + /* cause= */ null, + PlaybackException.ERROR_CODE_DECODING_FAILED)) + .build()); + } + + @Test + public void stateBuilderBuild_multiplePlaylistItemsWithSameIds_throwsException() { + Object uid = new Object(); + + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(uid).build(), + new SimpleBasePlayer.PlaylistItem.Builder(uid).build())) + .build()); + } + + @Test + public void stateBuilderBuild_adGroupIndexWithUnsetAdIndex_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ C.INDEX_UNSET, /* adIndexInAdGroup= */ 0)); + } + + @Test + public void stateBuilderBuild_unsetAdGroupIndexWithSetAdIndex_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ C.INDEX_UNSET)); + } + + @Test + public void stateBuilderBuild_unsetAdGroupIndexAndAdIndex_doesNotThrow() { + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setCurrentAd(/* adGroupIndex= */ C.INDEX_UNSET, /* adIndexInAdGroup= */ C.INDEX_UNSET) + .build(); + + assertThat(state.currentAdGroupIndex).isEqualTo(C.INDEX_UNSET); + assertThat(state.currentAdIndexInAdGroup).isEqualTo(C.INDEX_UNSET); + } + + @Test + public void stateBuilderBuild_returnsAdvancingContentPositionWhenPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setContentPositionMs(4000) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .build(); + long position1 = state.contentPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.contentPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(8000); + } + + @Test + public void stateBuilderBuild_returnsConstantContentPositionWhenNotPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build())) + .setContentPositionMs(4000) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + long position1 = state.contentPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.contentPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(4000); + } + + @Test + public void stateBuilderBuild_returnsAdvancingAdPositionWhenPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .setAdPositionMs(4000) + .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_READY) + // This should be ignored as ads are assumed to be played with unit speed. + .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f)) + .build(); + long position1 = state.adPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.adPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(6000); + } + + @Test + public void stateBuilderBuild_returnsConstantAdPositionWhenNotPlaying() { + SystemClock.setCurrentTimeMillis(10000); + + SimpleBasePlayer.State state = + new SimpleBasePlayer.State.Builder() + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), + /* adGroupTimesUs= */ 123) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2)) + .build())) + .build())) + .setCurrentAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1) + .setAdPositionMs(4000) + .setPlaybackState(Player.STATE_BUFFERING) + .build(); + long position1 = state.adPositionMsSupplier.get(); + SystemClock.setCurrentTimeMillis(12000); + long position2 = state.adPositionMsSupplier.get(); + + assertThat(position1).isEqualTo(4000); + assertThat(position2).isEqualTo(4000); + } + + @Test + public void playlistItemBuilderBuild_setsCorrectValues() { + Object uid = new Object(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Object manifest = new Object(); + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); + ImmutableList periods = + ImmutableList.of(new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()).build()); + + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(uid) + .setTracks(tracks) + .setMediaItem(mediaItem) + .setMediaMetadata(mediaMetadata) + .setManifest(manifest) + .setLiveConfiguration(liveConfiguration) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods(periods) + .build(); + + assertThat(playlistItem.uid).isEqualTo(uid); + assertThat(playlistItem.tracks).isEqualTo(tracks); + assertThat(playlistItem.mediaItem).isEqualTo(mediaItem); + assertThat(playlistItem.mediaMetadata).isEqualTo(mediaMetadata); + assertThat(playlistItem.manifest).isEqualTo(manifest); + assertThat(playlistItem.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(playlistItem.presentationStartTimeMs).isEqualTo(12); + assertThat(playlistItem.windowStartTimeMs).isEqualTo(23); + assertThat(playlistItem.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(playlistItem.isSeekable).isTrue(); + assertThat(playlistItem.isDynamic).isTrue(); + assertThat(playlistItem.defaultPositionUs).isEqualTo(456_789); + assertThat(playlistItem.durationUs).isEqualTo(500_000); + assertThat(playlistItem.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(playlistItem.isPlaceholder).isTrue(); + assertThat(playlistItem.periods).isEqualTo(periods); + } + + @Test + public void playlistItemBuilderBuild_presentationStartTimeIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPresentationStartTimeMs(12) + .build()); + } + + @Test + public void playlistItemBuilderBuild_windowStartTimeIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setWindowStartTimeMs(12) + .build()); + } + + @Test + public void playlistItemBuilderBuild_elapsedEpochOffsetIfNotLive_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setElapsedRealtimeEpochOffsetMs(12) + .build()); + } + + @Test + public void + playlistItemBuilderBuild_windowStartTimeLessThanPresentationStartTime_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setLiveConfiguration(MediaItem.LiveConfiguration.UNSET) + .setWindowStartTimeMs(12) + .setPresentationStartTimeMs(13) + .build()); + } + + @Test + public void playlistItemBuilderBuild_multiplePeriodsWithSameUid_throwsException() { + Object uid = new Object(); + + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(uid).build(), + new SimpleBasePlayer.PeriodData.Builder(uid).build())) + .build()); + } + + @Test + public void playlistItemBuilderBuild_defaultPositionGreaterThanDuration_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setDefaultPositionUs(16) + .setDurationUs(15) + .build()); + } + + @Test + public void periodDataBuilderBuild_setsCorrectValues() { + Object uid = new Object(); + AdPlaybackState adPlaybackState = + new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666); + + SimpleBasePlayer.PeriodData periodData = + new SimpleBasePlayer.PeriodData.Builder(uid) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState(adPlaybackState) + .build(); + + assertThat(periodData.uid).isEqualTo(uid); + assertThat(periodData.isPlaceholder).isTrue(); + assertThat(periodData.durationUs).isEqualTo(600_000); + assertThat(periodData.adPlaybackState).isEqualTo(adPlaybackState); } @Test @@ -101,6 +766,72 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { new Commands.Builder() .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) .build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + Object playlistItemUid = new Object(); + Object periodUid = new Object(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Object manifest = new Object(); + Size surfaceSize = new Size(480, 360); + MediaItem.LiveConfiguration liveConfiguration = + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build(); + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(playlistItemUid) + .setTracks(tracks) + .setMediaItem(mediaItem) + .setMediaMetadata(mediaMetadata) + .setManifest(manifest) + .setLiveConfiguration(liveConfiguration) + .setPresentationStartTimeMs(12) + .setWindowStartTimeMs(23) + .setElapsedRealtimeEpochOffsetMs(10234) + .setIsSeekable(true) + .setIsDynamic(true) + .setDefaultPositionUs(456_789) + .setDurationUs(500_000) + .setPositionInFirstPeriodUs(100_000) + .setIsPlaceholder(true) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(periodUid) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666)) + .build())) + .build()); State state = new State.Builder() .setAvailableCommands(commands) @@ -108,8 +839,38 @@ public void getterMethods_noOtherMethodCalls_returnCurrentState() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setPlaylist(playlist) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setCurrentPeriodIndex(1) + .setContentPositionMs(contentPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) .build(); - SimpleBasePlayer player = + + Player player = new SimpleBasePlayer(Looper.myLooper()) { @Override protected State getState() { @@ -120,11 +881,178 @@ protected State getState() { assertThat(player.getApplicationLooper()).isEqualTo(Looper.myLooper()); assertThat(player.getAvailableCommands()).isEqualTo(commands); assertThat(player.getPlayWhenReady()).isTrue(); + assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_IDLE); + assertThat(player.getPlaybackSuppressionReason()) + .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + assertThat(player.getPlayerError()).isEqualTo(error); + assertThat(player.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL); + assertThat(player.getShuffleModeEnabled()).isTrue(); + assertThat(player.isLoading()).isFalse(); + assertThat(player.getSeekBackIncrement()).isEqualTo(5000); + assertThat(player.getSeekForwardIncrement()).isEqualTo(4000); + assertThat(player.getMaxSeekToPreviousPosition()).isEqualTo(3000); + assertThat(player.getPlaybackParameters()).isEqualTo(playbackParameters); + assertThat(player.getCurrentTracks()).isEqualTo(tracks); + assertThat(player.getTrackSelectionParameters()).isEqualTo(trackSelectionParameters); + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + assertThat(player.getPlaylistMetadata()).isEqualTo(playlistMetadata); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getDuration()).isEqualTo(500); + assertThat(player.getCurrentPosition()).isEqualTo(456); + assertThat(player.getBufferedPosition()).isEqualTo(499); + assertThat(player.getTotalBufferedDuration()).isEqualTo(567); + assertThat(player.isPlayingAd()).isFalse(); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(C.INDEX_UNSET); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(C.INDEX_UNSET); + assertThat(player.getContentPosition()).isEqualTo(456); + assertThat(player.getContentBufferedPosition()).isEqualTo(499); + assertThat(player.getAudioAttributes()).isEqualTo(audioAttributes); + assertThat(player.getVolume()).isEqualTo(0.5f); + assertThat(player.getVideoSize()).isEqualTo(videoSize); + assertThat(player.getCurrentCues()).isEqualTo(cueGroup); + assertThat(player.getDeviceInfo()).isEqualTo(deviceInfo); + assertThat(player.getDeviceVolume()).isEqualTo(5); + assertThat(player.isDeviceMuted()).isTrue(); + assertThat(player.getSurfaceSize()).isEqualTo(surfaceSize); + Timeline timeline = player.getCurrentTimeline(); + assertThat(timeline.getPeriodCount()).isEqualTo(2); + assertThat(timeline.getWindowCount()).isEqualTo(2); + Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(0); + assertThat(window.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(window.firstPeriodIndex).isEqualTo(0); + assertThat(window.isDynamic).isFalse(); + assertThat(window.isPlaceholder).isFalse(); + assertThat(window.isSeekable).isFalse(); + assertThat(window.lastPeriodIndex).isEqualTo(0); + assertThat(window.positionInFirstPeriodUs).isEqualTo(0); + assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET); + assertThat(window.liveConfiguration).isNull(); + assertThat(window.manifest).isNull(); + assertThat(window.mediaItem).isEqualTo(MediaItem.EMPTY); + window = timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()); + assertThat(window.defaultPositionUs).isEqualTo(456_789); + assertThat(window.durationUs).isEqualTo(500_000); + assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(10234); + assertThat(window.firstPeriodIndex).isEqualTo(1); + assertThat(window.isDynamic).isTrue(); + assertThat(window.isPlaceholder).isTrue(); + assertThat(window.isSeekable).isTrue(); + assertThat(window.lastPeriodIndex).isEqualTo(1); + assertThat(window.positionInFirstPeriodUs).isEqualTo(100_000); + assertThat(window.presentationStartTimeMs).isEqualTo(12); + assertThat(window.windowStartTimeMs).isEqualTo(23); + assertThat(window.liveConfiguration).isEqualTo(liveConfiguration); + assertThat(window.manifest).isEqualTo(manifest); + assertThat(window.mediaItem).isEqualTo(mediaItem); + assertThat(window.uid).isEqualTo(playlistItemUid); + Timeline.Period period = + timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(C.TIME_UNSET); + assertThat(period.isPlaceholder).isFalse(); + assertThat(period.positionInWindowUs).isEqualTo(0); + assertThat(period.windowIndex).isEqualTo(0); + assertThat(period.getAdGroupCount()).isEqualTo(0); + period = timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true); + assertThat(period.durationUs).isEqualTo(600_000); + assertThat(period.isPlaceholder).isTrue(); + assertThat(period.positionInWindowUs).isEqualTo(-100_000); + assertThat(period.windowIndex).isEqualTo(1); + assertThat(period.id).isEqualTo(periodUid); + assertThat(period.getAdGroupCount()).isEqualTo(2); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(555); + assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(666); + } + + @Test + public void getterMethods_duringAd_returnAdState() { + SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; + SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499; + SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567; + SimpleBasePlayer.PositionSupplier adPositionSupplier = () -> 321; + SimpleBasePlayer.PositionSupplier adBufferedPositionSupplier = () -> 345; + ImmutableList playlist = + ImmutableList.of( + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(), + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()) + .setDurationUs(500_000) + .setPeriods( + ImmutableList.of( + new SimpleBasePlayer.PeriodData.Builder(/* uid= */ new Object()) + .setIsPlaceholder(true) + .setDurationUs(600_000) + .setAdPlaybackState( + new AdPlaybackState( + /* adsId= */ new Object(), /* adGroupTimesUs= */ 555, 666) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1) + .withAdDurationsUs( + /* adGroupIndex= */ 0, /* adDurationsUs... */ 700_000) + .withAdDurationsUs( + /* adGroupIndex= */ 1, /* adDurationsUs... */ 800_000)) + .build())) + .build()); + State state = + new State.Builder() + .setPlaylist(playlist) + .setCurrentMediaItemIndex(1) + .setCurrentAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0) + .setContentPositionMs(contentPositionSupplier) + .setContentBufferedPositionMs(contentBufferedPositionSupplier) + .setTotalBufferedDurationMs(totalBufferedPositionSupplier) + .setAdPositionMs(adPositionSupplier) + .setAdBufferedPositionMs(adBufferedPositionSupplier) + .build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(1); + assertThat(player.getDuration()).isEqualTo(800); + assertThat(player.getCurrentPosition()).isEqualTo(321); + assertThat(player.getBufferedPosition()).isEqualTo(345); + assertThat(player.getTotalBufferedDuration()).isEqualTo(567); + assertThat(player.isPlayingAd()).isTrue(); + assertThat(player.getCurrentAdGroupIndex()).isEqualTo(1); + assertThat(player.getCurrentAdIndexInAdGroup()).isEqualTo(0); + assertThat(player.getContentPosition()).isEqualTo(456); + assertThat(player.getContentBufferedPosition()).isEqualTo(499); + } + + @Test + public void getterMethods_withEmptyTimeline_returnPlaceholderValues() { + State state = new State.Builder().setCurrentMediaItemIndex(4).build(); + + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + }; + + assertThat(player.getCurrentTracks()).isEqualTo(Tracks.EMPTY); + assertThat(player.getMediaMetadata()).isEqualTo(MediaMetadata.EMPTY); + assertThat(player.getCurrentPeriodIndex()).isEqualTo(4); + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(4); } @SuppressWarnings("deprecation") // Verifying deprecated listener call. @Test - public void invalidateState_updatesStateAndInformsListeners() { + public void invalidateState_updatesStateAndInformsListeners() throws Exception { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); State state1 = new State.Builder() .setAvailableCommands(new Commands.Builder().addAllCommands().build()) @@ -132,14 +1060,108 @@ public void invalidateState_updatesStateAndInformsListeners() { /* playWhenReady= */ true, /* playWhenReadyChangeReason= */ Player .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) + .setPlaybackState(Player.STATE_READY) + .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE) + .setPlayerError(null) + .setRepeatMode(Player.REPEAT_MODE_ONE) + .setShuffleModeEnabled(false) + .setIsLoading(true) + .setSeekBackIncrementMs(7000) + .setSeekForwardIncrementMs(2000) + .setMaxSeekToPreviousPositionMs(8000) + .setPlaybackParameters(PlaybackParameters.DEFAULT) + .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT) + .setAudioAttributes(AudioAttributes.DEFAULT) + .setVolume(1f) + .setVideoSize(VideoSize.UNKNOWN) + .setCurrentCues(CueGroup.EMPTY_TIME_ZERO) + .setDeviceInfo(DeviceInfo.UNKNOWN) + .setDeviceVolume(0) + .setIsDeviceMuted(false) + .setPlaylist(ImmutableList.of(playlistItem0)) + .setPlaylistMetadata(MediaMetadata.EMPTY) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(8_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1) + .setMediaItem(mediaItem1) + .setMediaMetadata(mediaMetadata) + .setTracks(tracks) + .build(); + Commands commands = + new Commands.Builder() + .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE) .build(); - Commands commands = new Commands.Builder().add(Player.COMMAND_GET_TEXT).build(); + PlaybackException error = + new PlaybackException( + /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f); + TrackSelectionParameters trackSelectionParameters = + TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + .buildUpon() + .setMaxVideoBitrate(1000) + .build(); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(); + VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400); + CueGroup cueGroup = + new CueGroup( + ImmutableList.of(new Cue.Builder().setText("text").build()), + /* presentationTimeUs= */ 123); + Metadata timedMetadata = + new Metadata(/* presentationTimeUs= */ 42, new FakeMetadataEntry("data")); + Size surfaceSize = new Size(480, 360); + DeviceInfo deviceInfo = + new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_LOCAL, /* minVolume= */ 3, /* maxVolume= */ 7); + MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); State state2 = new State.Builder() .setAvailableCommands(commands) .setPlayWhenReady( /* playWhenReady= */ false, - /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE) + /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) + .setPlaybackState(Player.STATE_IDLE) + .setPlaybackSuppressionReason( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) + .setPlayerError(error) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .setShuffleModeEnabled(true) + .setIsLoading(false) + .setSeekBackIncrementMs(5000) + .setSeekForwardIncrementMs(4000) + .setMaxSeekToPreviousPositionMs(3000) + .setPlaybackParameters(playbackParameters) + .setTrackSelectionParameters(trackSelectionParameters) + .setAudioAttributes(audioAttributes) + .setVolume(0.5f) + .setVideoSize(videoSize) + .setCurrentCues(cueGroup) + .setDeviceInfo(deviceInfo) + .setDeviceVolume(5) + .setIsDeviceMuted(true) + .setAudioSessionId(78) + .setSkipSilenceEnabled(true) + .setSurfaceSize(surfaceSize) + .setNewlyRenderedFirstFrame(true) + .setTimedMetadata(timedMetadata) + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setPlaylistMetadata(playlistMetadata) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(12_000) + .setPositionDiscontinuity( + Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 11_500) .build(); AtomicBoolean returnState2 = new AtomicBoolean(); SimpleBasePlayer player = @@ -156,18 +1178,521 @@ protected State getState() { returnState2.set(true); player.invalidateState(); - - // Verify updated state. - assertThat(player.getAvailableCommands()).isEqualTo(commands); + // Verify state2 is used. assertThat(player.getPlayWhenReady()).isFalse(); - // Verify listener calls. + // Idle Looper to ensure all callbacks (including onEvents) are delivered. + ShadowLooper.idleMainLooper(); + + // Assert listener calls. verify(listener).onAvailableCommandsChanged(commands); verify(listener) .onPlayWhenReadyChanged( - /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); + /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); verify(listener) .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE); + verify(listener).onPlaybackStateChanged(Player.STATE_IDLE); + verify(listener) + .onPlaybackSuppressionReasonChanged( + Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS); + verify(listener).onIsPlayingChanged(false); + verify(listener).onPlayerError(error); + verify(listener).onPlayerErrorChanged(error); + verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL); + verify(listener).onShuffleModeEnabledChanged(true); + verify(listener).onLoadingChanged(false); + verify(listener).onIsLoadingChanged(false); + verify(listener).onSeekBackIncrementChanged(5000); + verify(listener).onSeekForwardIncrementChanged(4000); + verify(listener).onMaxSeekToPreviousPositionChanged(3000); + verify(listener).onPlaybackParametersChanged(playbackParameters); + verify(listener).onTrackSelectionParametersChanged(trackSelectionParameters); + verify(listener).onAudioAttributesChanged(audioAttributes); + verify(listener).onVolumeChanged(0.5f); + verify(listener).onVideoSizeChanged(videoSize); + verify(listener).onCues(cueGroup.cues); + verify(listener).onCues(cueGroup); + verify(listener).onDeviceInfoChanged(deviceInfo); + verify(listener).onDeviceVolumeChanged(/* volume= */ 5, /* muted= */ true); + verify(listener) + .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + verify(listener).onMediaMetadataChanged(mediaMetadata); + verify(listener).onTracksChanged(tracks); + verify(listener).onPlaylistMetadataChanged(playlistMetadata); + verify(listener).onAudioSessionIdChanged(78); + verify(listener).onRenderedFirstFrame(); + verify(listener).onMetadata(timedMetadata); + verify(listener).onSurfaceSizeChanged(surfaceSize.getWidth(), surfaceSize.getHeight()); + verify(listener).onSkipSilenceEnabledChanged(true); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 8_000, + /* contentPositionMs= */ 8_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 11_500, + /* contentPositionMs= */ 11_500, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener) + .onEvents( + player, + new Player.Events( + new FlagSet.Builder() + .addAll( + Player.EVENT_TIMELINE_CHANGED, + Player.EVENT_MEDIA_ITEM_TRANSITION, + Player.EVENT_TRACKS_CHANGED, + Player.EVENT_IS_LOADING_CHANGED, + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_REPEAT_MODE_CHANGED, + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + Player.EVENT_PLAYER_ERROR, + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + Player.EVENT_AVAILABLE_COMMANDS_CHANGED, + Player.EVENT_MEDIA_METADATA_CHANGED, + Player.EVENT_PLAYLIST_METADATA_CHANGED, + Player.EVENT_SEEK_BACK_INCREMENT_CHANGED, + Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED, + Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED, + Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED, + Player.EVENT_AUDIO_ATTRIBUTES_CHANGED, + Player.EVENT_AUDIO_SESSION_ID, + Player.EVENT_VOLUME_CHANGED, + Player.EVENT_SKIP_SILENCE_ENABLED_CHANGED, + Player.EVENT_SURFACE_SIZE_CHANGED, + Player.EVENT_VIDEO_SIZE_CHANGED, + Player.EVENT_RENDERED_FIRST_FRAME, + Player.EVENT_CUES, + Player.EVENT_METADATA, + Player.EVENT_DEVICE_INFO_CHANGED, + Player.EVENT_DEVICE_VOLUME_CHANGED) + .build())); verifyNoMoreInteractions(listener); + // Assert that we actually called all listeners. + for (Method method : Player.Listener.class.getDeclaredMethods()) { + if (method.getName().equals("onSeekProcessed")) { + continue; + } + method.invoke(verify(listener), getAnyArguments(method)); + } + } + + @Test + public void invalidateState_withPlaylistItemDetailChange_reportsTimelineSourceUpdate() { + Object mediaItemUid0 = new Object(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).build(); + Object mediaItemUid1 = new Object(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).build(); + State state1 = + new State.Builder().setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)).build(); + SimpleBasePlayer.PlaylistItem playlistItem1Updated = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setDurationUs(10_000).build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1Updated)) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + } + + @Test + public void invalidateState_withCurrentMediaItemRemoval_reportsDiscontinuityReasonRemoved() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0).setMediaItem(mediaItem0).build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(5000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(2000) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 5000, + /* contentPositionMs= */ 5000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 2000, + /* contentPositionMs= */ 2000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_REMOVE); + verify(listener) + .onMediaItemTransition(mediaItem0, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + } + + @Test + public void + invalidateState_withTransitionFromEndOfItem_reportsDiscontinuityReasonAutoTransition() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + .setMediaItem(mediaItem0) + .setDurationUs(50_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(50) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(10) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 50, + /* contentPositionMs= */ 50, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 10, + /* contentPositionMs= */ 10, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_AUTO); + } + + @Test + public void invalidateState_withTransitionFromMiddleOfItem_reportsDiscontinuityReasonSkip() { + Object mediaItemUid0 = new Object(); + MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem0 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid0) + .setMediaItem(mediaItem0) + .setDurationUs(50_000) + .build(); + Object mediaItemUid1 = new Object(); + MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build(); + SimpleBasePlayer.PlaylistItem playlistItem1 = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid1).setMediaItem(mediaItem1).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(20) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem0, playlistItem1)) + .setCurrentMediaItemIndex(1) + .setContentPositionMs(10) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid0, + /* mediaItemIndex= */ 0, + mediaItem0, + /* periodUid= */ mediaItemUid0, + /* periodIndex= */ 0, + /* positionMs= */ 20, + /* contentPositionMs= */ 20, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid1, + /* mediaItemIndex= */ 1, + mediaItem1, + /* periodUid= */ mediaItemUid1, + /* periodIndex= */ 1, + /* positionMs= */ 10, + /* contentPositionMs= */ 10, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_SKIP); + verify(listener) + .onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + } + + @Test + public void invalidateState_withRepeatingItem_reportsDiscontinuityReasonAutoTransition() { + Object mediaItemUid = new Object(); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + .setMediaItem(mediaItem) + .setDurationUs(5_000_000) + .build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(5_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(0) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 5_000, + /* contentPositionMs= */ 5_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 0, + /* contentPositionMs= */ 0, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_AUTO_TRANSITION); + verify(listener).onMediaItemTransition(mediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT); + } + + @Test + public void invalidateState_withDiscontinuityInsideItem_reportsDiscontinuityReasonInternal() { + Object mediaItemUid = new Object(); + MediaItem mediaItem = new MediaItem.Builder().setMediaId("0").build(); + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(mediaItemUid) + .setMediaItem(mediaItem) + .setDurationUs(5_000_000) + .build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(3_000) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener) + .onPositionDiscontinuity( + /* oldPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 1_000, + /* contentPositionMs= */ 1_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + /* newPosition= */ new Player.PositionInfo( + mediaItemUid, + /* mediaItemIndex= */ 0, + mediaItem, + /* periodUid= */ mediaItemUid, + /* periodIndex= */ 0, + /* positionMs= */ 3_000, + /* contentPositionMs= */ 3_000, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET), + Player.DISCONTINUITY_REASON_INTERNAL); + verify(listener, never()).onMediaItemTransition(any(), anyInt()); + } + + @Test + public void invalidateState_withMinorPositionDrift_doesNotReportsDiscontinuity() { + SimpleBasePlayer.PlaylistItem playlistItem = + new SimpleBasePlayer.PlaylistItem.Builder(/* uid= */ new Object()).build(); + State state1 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_000) + .build(); + State state2 = + new State.Builder() + .setPlaylist(ImmutableList.of(playlistItem)) + .setCurrentMediaItemIndex(0) + .setContentPositionMs(1_500) + .build(); + AtomicBoolean returnState2 = new AtomicBoolean(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return returnState2.get() ? state2 : state1; + } + }; + player.invalidateState(); + Listener listener = mock(Listener.class); + player.addListener(listener); + + returnState2.set(true); + player.invalidateState(); + + // Assert listener call. + verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + verify(listener, never()).onMediaItemTransition(any(), anyInt()); } @Test @@ -403,4 +1928,23 @@ protected ListenableFuture handleSetPlayWhenReady(boolean playWhenReady) { assertThat(callForwarded.get()).isFalse(); } + + private static Object[] getAnyArguments(Method method) { + Object[] arguments = new Object[method.getParameterCount()]; + Class[] argumentTypes = method.getParameterTypes(); + for (int i = 0; i < arguments.length; i++) { + if (argumentTypes[i].equals(Integer.TYPE)) { + arguments[i] = anyInt(); + } else if (argumentTypes[i].equals(Long.TYPE)) { + arguments[i] = anyLong(); + } else if (argumentTypes[i].equals(Float.TYPE)) { + arguments[i] = anyFloat(); + } else if (argumentTypes[i].equals(Boolean.TYPE)) { + arguments[i] = anyBoolean(); + } else { + arguments[i] = any(); + } + } + return arguments; + } }