media/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
bachinger d681e264aa Skip played server side inserted ads in a single period window
This change makes sure played server side ads are skipped in a single period
timeline. It avoids creating an ad-MediaPeriodInfo for played postrolls and
creates a content info instead. It also sets the end position for content infos
that terminate the stream before the stream is actually finished. This prevents
the player from continue playing the remaining media delivered by the
MediaPeriod.

We also make sure that the discontinuity of played ads are not reported because
there is actually no discontinuity.

#minor-release

PiperOrigin-RevId: 428734387
2022-02-17 11:52:23 +00:00

12301 lines
537 KiB
Java

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer;
import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES;
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_GET_TEXT;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_TRACK_INFOS;
import static androidx.media3.common.Player.COMMAND_GET_VOLUME;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
import static androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE;
import static androidx.media3.common.Player.COMMAND_SET_VOLUME;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US;
import static androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
import static androidx.media3.test.utils.TestUtil.assertTimelinesSame;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilStartOfMediaItem;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPositionDiscontinuity;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.not;
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.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.content.Intent;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Looper;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.IllegalSeekPositionException;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Player.Listener;
import androidx.media3.common.Player.PositionInfo;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.TrackSelectionArray;
import androidx.media3.common.TracksInfo;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.source.ClippingMediaSource;
import androidx.media3.exoplayer.source.CompositeMediaSource;
import androidx.media3.exoplayer.source.ConcatenatingMediaSource;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.Allocation;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.Loader;
import androidx.media3.extractor.metadata.id3.BinaryFrame;
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
import androidx.media3.test.utils.Action;
import androidx.media3.test.utils.ActionSchedule;
import androidx.media3.test.utils.ActionSchedule.PlayerRunnable;
import androidx.media3.test.utils.ActionSchedule.PlayerTarget;
import androidx.media3.test.utils.ExoPlayerTestRunner;
import androidx.media3.test.utils.FakeAdaptiveDataSet;
import androidx.media3.test.utils.FakeAdaptiveMediaSource;
import androidx.media3.test.utils.FakeChunkSource;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.FakeMediaClockRenderer;
import androidx.media3.test.utils.FakeMediaPeriod;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeMediaSourceFactory;
import androidx.media3.test.utils.FakeRenderer;
import androidx.media3.test.utils.FakeSampleStream;
import androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem;
import androidx.media3.test.utils.FakeShuffleOrder;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
import androidx.media3.test.utils.FakeTrackSelection;
import androidx.media3.test.utils.FakeTrackSelector;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.NoUidTimeline;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowAudioManager;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link ExoPlayer}. */
@RunWith(AndroidJUnit4.class)
public final class ExoPlayerTest {
private static final String TAG = "ExoPlayerTest";
/**
* For tests that rely on the player transitioning to the ended state, the duration in
* milliseconds after starting the player before the test will time out. This is to catch cases
* where the player under test is not making progress, in which case the test should fail.
*/
private static final int TIMEOUT_MS = 10_000;
private Context context;
private Timeline placeholderTimeline;
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
placeholderTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(0).build());
}
/**
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
* error.
*/
@Test
public void playEmptyTimeline() throws Exception {
Timeline timeline = Timeline.EMPTY;
Timeline expectedMaskingTimeline =
new MaskingMediaSource.PlaceholderTimeline(FakeMediaSource.FAKE_MEDIA_ITEM);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_UNKNOWN);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = inOrder(mockListener);
inOrder
.verify(mockListener)
.onTimelineChanged(
argThat(noUid(expectedMaskingTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(renderer.getFormatsRead()).isEmpty();
assertThat(renderer.sampleBufferReadCount).isEqualTo(0);
assertThat(renderer.isEnded).isFalse();
}
/** Tests playback of a source that exposes a single period. */
@Test
public void playSinglePeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline();
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = Mockito.inOrder(mockListener);
inOrder
.verify(mockListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockListener)
.onTracksChanged(
eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(renderer.sampleBufferReadCount).isEqualTo(1);
assertThat(renderer.isEnded).isTrue();
}
/** Tests playback of a source that exposes three periods. */
@Test
public void playMultiPeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = Mockito.inOrder(mockPlayerListener);
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(new FakeMediaSource.InitialTimeline(timeline))),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener, times(2))
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(renderer.getFormatsRead())
.containsExactly(
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(renderer.sampleBufferReadCount).isEqualTo(3);
assertThat(renderer.isEnded).isTrue();
}
/** Tests playback of periods with very short duration. */
@Test
public void playShortDurationPeriods() throws Exception {
// TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period.
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 100, /* id= */ 0));
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = inOrder(mockPlayerListener);
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener, times(99))
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(renderer.getFormatsRead()).hasSize(100);
assertThat(renderer.sampleBufferReadCount).isEqualTo(100);
assertThat(renderer.isEnded).isTrue();
}
@Test
public void renderersLifecycle_renderersThatAreNeverEnabled_areNotReset() throws Exception {
Timeline timeline = new FakeTimeline();
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
final FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderers(videoRenderer, audioRenderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
assertThat(audioRenderer.enabledCount).isEqualTo(1);
assertThat(audioRenderer.resetCount).isEqualTo(1);
assertThat(videoRenderer.enabledCount).isEqualTo(0);
assertThat(videoRenderer.resetCount).isEqualTo(0);
}
@Test
public void renderersLifecycle_setForegroundMode_resetsDisabledRenderersThatHaveBeenEnabled()
throws Exception {
Timeline timeline = new FakeTimeline();
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
final FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT);
ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderers(videoRenderer, audioRenderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(
timeline, ExoPlayerTestRunner.AUDIO_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
player.play();
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setForegroundMode(/* foregroundMode= */ true);
// Only the video renderer that is disabled in the second media item has been reset.
assertThat(audioRenderer.resetCount).isEqualTo(0);
assertThat(videoRenderer.resetCount).isEqualTo(1);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// After release the audio renderer is reset as well.
assertThat(audioRenderer.enabledCount).isEqualTo(1);
assertThat(audioRenderer.resetCount).isEqualTo(1);
assertThat(videoRenderer.enabledCount).isEqualTo(1);
assertThat(videoRenderer.resetCount).isEqualTo(1);
assertThat(textRenderer.enabledCount).isEqualTo(0);
assertThat(textRenderer.resetCount).isEqualTo(0);
}
@Test
public void renderersLifecycle_selectTextTracksWhilePlaying_textRendererEnabledAndReset()
throws Exception {
Timeline timeline = new FakeTimeline();
final FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
final FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT);
Format textFormat =
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).setLanguage("en").build();
ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderers(audioRenderer, textRenderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT, textFormat)));
player.prepare();
player.play();
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
// Only the audio renderer enabled so far.
assertThat(audioRenderer.enabledCount).isEqualTo(1);
assertThat(textRenderer.enabledCount).isEqualTo(0);
player.setTrackSelectionParameters(
player.getTrackSelectionParameters().buildUpon().setPreferredTextLanguage("en").build());
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
assertThat(audioRenderer.enabledCount).isEqualTo(1);
assertThat(audioRenderer.resetCount).isEqualTo(1);
assertThat(textRenderer.enabledCount).isEqualTo(1);
assertThat(textRenderer.resetCount).isEqualTo(1);
assertThat(videoRenderer.enabledCount).isEqualTo(0);
assertThat(videoRenderer.resetCount).isEqualTo(0);
}
@Test
public void renderersLifecycle_seekTo_resetsDisabledRenderersIfRequired() throws Exception {
Timeline timeline = new FakeTimeline();
final FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
final FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT);
Format textFormat =
new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).setLanguage("en").build();
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(videoRenderer, audioRenderer, textRenderer)
.build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setTrackSelectionParameters(
player.getTrackSelectionParameters().buildUpon().setPreferredTextLanguage("en").build());
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT, textFormat)));
player.prepare();
player.play();
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
// Disable text renderer by selecting a language that is not available.
player.setTrackSelectionParameters(
player.getTrackSelectionParameters().buildUpon().setPreferredTextLanguage("de").build());
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1000);
runUntilPlaybackState(player, Player.STATE_READY);
// Expect formerly enabled renderers to be reset after seek.
assertThat(textRenderer.resetCount).isEqualTo(1);
assertThat(audioRenderer.resetCount).isEqualTo(0);
assertThat(videoRenderer.resetCount).isEqualTo(0);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Verify that the text renderer has not been reset a second time.
assertThat(audioRenderer.enabledCount).isEqualTo(2);
assertThat(audioRenderer.resetCount).isEqualTo(1);
assertThat(textRenderer.enabledCount).isEqualTo(1);
assertThat(textRenderer.resetCount).isEqualTo(1);
assertThat(videoRenderer.enabledCount).isEqualTo(0);
assertThat(videoRenderer.resetCount).isEqualTo(0);
}
/**
* Tests that the player does not unnecessarily reset renderers when playing a multi-period
* source.
*/
@Test
public void readAheadToEndDoesNotResetRenderer() throws Exception {
// Use sufficiently short periods to ensure the player attempts to read all at once.
TimelineWindowDefinition windowDefinition0 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ false,
/* isDynamic= */ false,
/* durationUs= */ 100_000);
TimelineWindowDefinition windowDefinition1 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ false,
/* isDynamic= */ false,
/* durationUs= */ 100_000);
TimelineWindowDefinition windowDefinition2 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ false,
/* isDynamic= */ false,
/* durationUs= */ 100_000);
Timeline timeline = new FakeTimeline(windowDefinition0, windowDefinition1, windowDefinition2);
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeMediaClockRenderer audioRenderer =
new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) {
@Override
public long getPositionUs() {
// Simulate the playback position lagging behind the reading position: the renderer
// media clock position will be the start of the timeline until the stream is set to be
// final, at which point it jumps to the end of the timeline allowing the playing period
// to advance.
return isCurrentStreamFinal() ? 30 : 0;
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
@Override
public boolean isEnded() {
return videoRenderer.isEnded();
}
};
ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderers(videoRenderer, audioRenderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSource(
new FakeMediaSource(
timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = inOrder(mockPlayerListener);
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(new FakeMediaSource.InitialTimeline(timeline))),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener, times(2))
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(audioRenderer.positionResetCount).isEqualTo(1);
assertThat(videoRenderer.isEnded).isTrue();
assertThat(audioRenderer.isEnded).isTrue();
}
@Test
public void resettingMediaSourcesGivesFreshSourceInfo() throws Exception {
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
Timeline firstTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 1_000_000_000));
MediaSource firstSource = new FakeMediaSource(firstTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
AtomicBoolean secondSourcePrepared = new AtomicBoolean();
MediaSource secondSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
public synchronized void prepareSourceInternal(
@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
secondSourcePrepared.set(true);
}
};
Timeline thirdTimeline = new FakeTimeline();
MediaSource thirdSource = new FakeMediaSource(thirdTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
Player.Listener mockPlayerListener = mock(Player.Listener.class);
player.addListener(mockPlayerListener);
player.setMediaSource(firstSource);
player.prepare();
player.play();
runUntilTimelineChanged(player);
player.setMediaSource(secondSource);
runMainLooperUntil(secondSourcePrepared::get);
player.setMediaSource(thirdSource);
runUntilPlaybackState(player, Player.STATE_ENDED);
// The first source's preparation completed with a real timeline. When the second source was
// prepared, it immediately exposed a placeholder timeline, but the source info refresh from the
// second source was suppressed as we replace it with the third source before the update
// arrives.
InOrder inOrder = inOrder(mockPlayerListener);
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(firstTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockPlayerListener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockPlayerListener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(mockPlayerListener)
.onTimelineChanged(
argThat(noUid(thirdTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockPlayerListener)
.onTracksChanged(
eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any());
assertThat(renderer.isEnded).isTrue();
}
@Test
public void repeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
AnalyticsListener mockAnalyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(mockAnalyticsListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
runUntilTimelineChanged(player);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 1);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 1);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 2);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 2);
player.setRepeatMode(Player.REPEAT_MODE_ALL);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 0);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 0);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 0);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 1);
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 2);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<AnalyticsListener.EventTime> eventTimes =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(mockAnalyticsListener, times(10))
.onMediaItemTransition(eventTimes.capture(), any(), anyInt());
assertThat(
eventTimes.getAllValues().stream()
.map(eventTime -> eventTime.currentWindowIndex)
.collect(Collectors.toList()))
.containsExactly(0, 1, 1, 2, 2, 0, 0, 0, 1, 2)
.inOrder();
assertThat(renderer.isEnded).isTrue();
}
@Test
public void shuffleModeEnabledChanges() throws Exception {
Timeline fakeTimeline = new FakeTimeline();
MediaSource[] fakeMediaSources = {
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)
};
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources);
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setRepeatMode(Player.REPEAT_MODE_ALL)
.playUntilStartOfMediaItem(/* mediaItemIndex= */ 1)
.setShuffleModeEnabled(true)
.playUntilStartOfMediaItem(/* mediaItemIndex= */ 1)
.setShuffleModeEnabled(false)
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(renderer.isEnded).isTrue();
}
@Test
public void adGroupWithLoadError_noFurtherAdGroup_isSkipped() throws Exception {
AdPlaybackState initialAdPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ 5 * C.MICROS_PER_SECOND);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
initialAdPlaybackState));
AdPlaybackState errorAdPlaybackState =
initialAdPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
final Timeline adErrorTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
errorAdPlaybackState));
final FakeMediaSource fakeMediaSource =
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setMediaSource(fakeMediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
fakeMediaSource.setNewSourceInfo(adErrorTimeline);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
Timeline.Window window =
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, new Timeline.Window());
Timeline.Period period =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true);
player.release();
// Content to content transition is ignored.
verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
}
@Test
public void adGroupWithLoadError_withFurtherAdGroup_isSkipped() throws Exception {
AdPlaybackState initialAdPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ 5 * C.MICROS_PER_SECOND,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ 8 * C.MICROS_PER_SECOND);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
initialAdPlaybackState));
AdPlaybackState errorAdPlaybackState =
initialAdPlaybackState.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0);
final Timeline adErrorTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
errorAdPlaybackState));
final FakeMediaSource fakeMediaSource =
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setMediaSource(fakeMediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
fakeMediaSource.setNewSourceInfo(adErrorTimeline);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
Timeline.Window window =
player.getCurrentTimeline().getWindow(/* windowIndex= */ 0, new Timeline.Window());
Timeline.Period period =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true);
player.release();
// There content to content discontinuity after the failed ad is suppressed.
PositionInfo positionInfoContentAtSuccessfulAd =
new PositionInfo(
window.uid,
/* mediaItemIndex= */ 0,
window.mediaItem,
period.uid,
/* periodIndex= */ 0,
/* positionMs= */ 8_000,
/* contentPositionMs= */ 8_000,
/* adGroupIndex= */ C.INDEX_UNSET,
/* adIndexInAdGroup= */ C.INDEX_UNSET);
PositionInfo positionInfoSuccessfulAdStart =
new PositionInfo(
window.uid,
/* mediaItemIndex= */ 0,
window.mediaItem,
period.uid,
/* periodIndex= */ 0,
/* positionMs= */ 0,
/* contentPositionMs= */ 8_000,
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0);
PositionInfo positionInfoSuccessfulAdEnd =
new PositionInfo(
window.uid,
/* mediaItemIndex= */ 0,
window.mediaItem,
period.uid,
/* periodIndex= */ 0,
/* positionMs= */ Util.usToMs(
period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)),
/* contentPositionMs= */ 8_000,
/* adGroupIndex= */ 1,
/* adIndexInAdGroup= */ 0);
verify(mockListener)
.onPositionDiscontinuity(
positionInfoContentAtSuccessfulAd,
positionInfoSuccessfulAdStart,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockListener)
.onPositionDiscontinuity(
positionInfoSuccessfulAdEnd,
positionInfoContentAtSuccessfulAd,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
}
@Test
public void periodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.setRepeatMode(Player.REPEAT_MODE_ALL)
.waitForPositionDiscontinuity()
.seek(0) // Seek with repeat mode set to Player.REPEAT_MODE_ALL.
.waitForPositionDiscontinuity()
.setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
.build();
new ExoPlayerTestRunner.Builder(context)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(renderer.isEnded).isTrue();
}
@Test
public void illegalSeekPositionDoesThrow() throws Exception {
final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1];
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
try {
player.seekTo(/* mediaItemIndex= */ 100, /* positionMs= */ 0);
} catch (IllegalSeekPositionException e) {
exception[0] = e;
}
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(exception[0]).isNotNull();
}
@Test
public void seekDiscontinuity() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
ActionSchedule actionSchedule = new ActionSchedule.Builder(TAG).seek(10).build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
}
@Test
public void seekDiscontinuityWithAdjustment() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
FakeMediaPeriod mediaPeriod =
new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false);
mediaPeriod.setSeekToUsOffset(10);
return mediaPeriod;
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.seek(10)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
@Test
public void internalDiscontinuityAtNewPosition() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
FakeMediaPeriod mediaPeriod =
new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher);
mediaPeriod.setDiscontinuityPositionUs(10);
return mediaPeriod;
}
};
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL);
}
@Test
public void internalDiscontinuityAtInitialPosition() throws Exception {
FakeTimeline timeline = new FakeTimeline();
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
FakeMediaPeriod mediaPeriod =
new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher);
// Set a discontinuity at the position this period is supposed to start at anyway.
mediaPeriod.setDiscontinuityPositionUs(
timeline.getWindow(/* windowIndex= */ 0, new Window()).positionInFirstPeriodUs);
return mediaPeriod;
}
};
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
// If the position is unchanged we do not expect the discontinuity to be reported externally.
testRunner.assertNoPositionDiscontinuities();
}
@Test
public void allActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(
new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
FakeTrackSelector trackSelector = new FakeTrackSelector();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertThat(trackSelection.isEnabled).isFalse();
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made once (1 period).
assertThat(createdTrackSelections).hasSize(2);
assertThat(numSelectionsEnabled).isEqualTo(2);
}
@Test
public void allActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource mediaSource =
new FakeMediaSource(
timeline, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
FakeTrackSelector trackSelector = new FakeTrackSelector();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertThat(trackSelection.isEnabled).isFalse();
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice (2 periods).
assertThat(createdTrackSelections).hasSize(4);
assertThat(numSelectionsEnabled).isEqualTo(4);
}
@Test
public void allActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(
new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final FakeTrackSelector trackSelector = new FakeTrackSelector();
ActionSchedule disableTrackAction =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.disableRenderer(0)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertThat(trackSelection.isEnabled).isFalse();
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice. The second time one renderer is
// disabled, so only one out of the two track selections is enabled.
assertThat(createdTrackSelections).hasSize(3);
assertThat(numSelectionsEnabled).isEqualTo(3);
}
@Test
public void allActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(
new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final FakeTrackSelector trackSelector =
new FakeTrackSelector(/* mayReuseTrackSelection= */ true);
ActionSchedule disableTrackAction =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.disableRenderer(0)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setRenderers(videoRenderer, audioRenderer)
.setTrackSelector(trackSelector)
.setActionSchedule(disableTrackAction)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
List<FakeTrackSelection> createdTrackSelections = trackSelector.getAllTrackSelections();
int numSelectionsEnabled = 0;
// Assert that all tracks selection are disabled at the end of the playback.
for (FakeTrackSelection trackSelection : createdTrackSelections) {
assertThat(trackSelection.isEnabled).isFalse();
numSelectionsEnabled += trackSelection.enableCount;
}
// There are 2 renderers, and track selections are made twice. The second time one renderer is
// disabled, and the selector re-uses the previous selection for the enabled renderer. So we
// expect two track selections, one of which will have been enabled twice.
assertThat(createdTrackSelections).hasSize(2);
assertThat(numSelectionsEnabled).isEqualTo(3);
}
@Test
public void dynamicTimelineChangeReason() throws Exception {
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000));
final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2))
.waitForTimelineChanged(
timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(placeholderTimeline, timeline, timeline2);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void resetMediaSourcesWithPositionResetAndShufflingUsesFirstPeriod() throws Exception {
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 100000));
ConcatenatingMediaSource firstMediaSource =
new ConcatenatingMediaSource(
/* isAtomic= */ false,
new FakeShuffleOrder(/* length= */ 2),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT));
ConcatenatingMediaSource secondMediaSource =
new ConcatenatingMediaSource(
/* isAtomic= */ false,
new FakeShuffleOrder(/* length= */ 2),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for first preparation and enable shuffling. Plays period 0.
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setShuffleModeEnabled(true)
// Set the second media source (with position reset).
// Plays period 1 and 0 because of the reversed fake shuffle order.
.setMediaSources(/* resetPosition= */ true, secondMediaSource)
.play()
.waitForPositionDiscontinuity()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0);
}
@Test
public void setPlaybackSpeedBeforePreparationCompletesSucceeds() throws Exception {
// Test that no exception is thrown when playback parameters are updated between creating a
// period and preparation of the period completing.
final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1);
final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1];
MediaSource mediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
// Defer completing preparation of the period until playback parameters have been set.
fakeMediaPeriodHolder[0] =
new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ true);
createPeriodCalledCountDownLatch.countDown();
return fakeMediaPeriodHolder[0];
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
// Block until createPeriod has been called on the fake media source.
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
try {
player.getClock().onThreadBlocked();
createPeriodCalledCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
})
// Set playback speed (while the fake media period is not yet prepared).
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f))
// Complete preparation of the fake media period.
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void seekBeforePreparationCompletes_seeksToCorrectPosition() throws Exception {
CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1);
FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1];
FakeMediaSource mediaSource =
new FakeMediaSource(/* timeline= */ null, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
// Defer completing preparation of the period until seek has been sent.
fakeMediaPeriodHolder[0] =
new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ true);
createPeriodCalledCountDownLatch.countDown();
return fakeMediaPeriodHolder[0];
}
};
AtomicLong positionWhenReady = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
// Ensure we use the MaskingMediaPeriod by delaying the initial timeline update.
.delay(1)
.executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline()))
.waitForTimelineChanged()
// Block until createPeriod has been called on the fake media source.
.executeRunnable(
() -> {
try {
createPeriodCalledCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
})
// Seek before preparation completes.
.seek(5000)
// Complete preparation of the fake media period.
.executeRunnable(() -> fakeMediaPeriodHolder[0].setPreparationComplete())
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionWhenReady.set(player.getCurrentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 0, /* positionMs= */ 2000)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionWhenReady.get()).isAtLeast(5000);
}
@Test
public void stop_withoutReset_doesNotResetPosition_correctMasking() throws Exception {
int[] currentMediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final FakeMediaSource mediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 1000)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[0] = player.getCurrentMediaItemIndex();
currentPosition[0] = player.getCurrentPosition();
bufferedPosition[0] = player.getBufferedPosition();
totalBufferedDuration[0] = player.getTotalBufferedDuration();
player.stop(/* reset= */ false);
currentMediaItemIndex[1] = player.getCurrentMediaItemIndex();
currentPosition[1] = player.getCurrentPosition();
bufferedPosition[1] = player.getBufferedPosition();
totalBufferedDuration[1] = player.getTotalBufferedDuration();
}
})
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[2] = player.getCurrentMediaItemIndex();
currentPosition[2] = player.getCurrentPosition();
bufferedPosition[2] = player.getBufferedPosition();
totalBufferedDuration[2] = player.getTotalBufferedDuration();
}
})
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
assertThat(currentMediaItemIndex[0]).isEqualTo(1);
assertThat(currentPosition[0]).isEqualTo(1000);
assertThat(bufferedPosition[0]).isEqualTo(10000);
assertThat(totalBufferedDuration[0]).isEqualTo(9000);
assertThat(currentMediaItemIndex[1]).isEqualTo(1);
assertThat(currentPosition[1]).isEqualTo(1000);
assertThat(bufferedPosition[1]).isEqualTo(1000);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
assertThat(currentMediaItemIndex[2]).isEqualTo(1);
assertThat(currentPosition[2]).isEqualTo(1000);
assertThat(bufferedPosition[2]).isEqualTo(1000);
assertThat(totalBufferedDuration[2]).isEqualTo(0);
}
@Test
public void stop_withoutReset_releasesMediaSource() throws Exception {
Timeline timeline = new FakeTimeline();
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ false)
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
mediaSource.assertReleased();
}
@Test
public void stop_withReset_doesResetPosition_correctMasking() throws Exception {
int[] currentMediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final FakeMediaSource mediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 1000)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[0] = player.getCurrentMediaItemIndex();
currentPosition[0] = player.getCurrentPosition();
bufferedPosition[0] = player.getBufferedPosition();
totalBufferedDuration[0] = player.getTotalBufferedDuration();
player.stop(/* reset= */ true);
currentMediaItemIndex[1] = player.getCurrentMediaItemIndex();
currentPosition[1] = player.getCurrentPosition();
bufferedPosition[1] = player.getBufferedPosition();
totalBufferedDuration[1] = player.getTotalBufferedDuration();
}
})
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[2] = player.getCurrentMediaItemIndex();
currentPosition[2] = player.getCurrentPosition();
bufferedPosition[2] = player.getBufferedPosition();
totalBufferedDuration[2] = player.getTotalBufferedDuration();
}
})
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_REMOVE);
assertThat(currentMediaItemIndex[0]).isEqualTo(1);
assertThat(currentPosition[0]).isGreaterThan(0);
assertThat(bufferedPosition[0]).isEqualTo(10000);
assertThat(totalBufferedDuration[0]).isEqualTo(10000 - currentPosition[0]);
assertThat(currentMediaItemIndex[1]).isEqualTo(0);
assertThat(currentPosition[1]).isEqualTo(0);
assertThat(bufferedPosition[1]).isEqualTo(0);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
assertThat(currentMediaItemIndex[2]).isEqualTo(0);
assertThat(currentPosition[2]).isEqualTo(0);
assertThat(bufferedPosition[2]).isEqualTo(0);
assertThat(totalBufferedDuration[2]).isEqualTo(0);
}
@Test
public void stop_withReset_releasesMediaSource() throws Exception {
Timeline timeline = new FakeTimeline();
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true)
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
mediaSource.assertReleased();
}
@Test
public void release_correctMasking() throws Exception {
int[] currentMediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] totalBufferedDuration = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final FakeMediaSource mediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 1000)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[0] = player.getCurrentMediaItemIndex();
currentPosition[0] = player.getCurrentPosition();
bufferedPosition[0] = player.getBufferedPosition();
totalBufferedDuration[0] = player.getTotalBufferedDuration();
player.release();
currentMediaItemIndex[1] = player.getCurrentMediaItemIndex();
currentPosition[1] = player.getCurrentPosition();
bufferedPosition[1] = player.getBufferedPosition();
totalBufferedDuration[1] = player.getTotalBufferedDuration();
}
})
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndex[2] = player.getCurrentMediaItemIndex();
currentPosition[2] = player.getCurrentPosition();
bufferedPosition[2] = player.getBufferedPosition();
totalBufferedDuration[2] = player.getTotalBufferedDuration();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS);
assertThat(currentMediaItemIndex[0]).isEqualTo(1);
assertThat(currentPosition[0]).isGreaterThan(0);
assertThat(bufferedPosition[0]).isEqualTo(10000);
assertThat(totalBufferedDuration[0]).isEqualTo(10000 - currentPosition[0]);
assertThat(currentMediaItemIndex[1]).isEqualTo(1);
assertThat(currentPosition[1]).isEqualTo(currentPosition[0]);
assertThat(bufferedPosition[1]).isEqualTo(1000);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
assertThat(currentMediaItemIndex[2]).isEqualTo(1);
assertThat(currentPosition[2]).isEqualTo(currentPosition[0]);
assertThat(bufferedPosition[2]).isEqualTo(1000);
assertThat(totalBufferedDuration[2]).isEqualTo(0);
}
@Test
public void settingNewStartPositionPossibleAfterStopWithReset() throws Exception {
Timeline timeline = new FakeTimeline();
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource secondSource =
new FakeMediaSource(secondTimeline, ExoPlayerTestRunner.VIDEO_FORMAT);
AtomicInteger mediaItemIndexAfterStop = new AtomicInteger();
AtomicLong positionAfterStop = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true)
.waitForPlaybackState(Player.STATE_IDLE)
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 1000)
.setMediaSources(secondSource)
.prepare()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndexAfterStop.set(player.getCurrentMediaItemIndex());
positionAfterStop.set(player.getCurrentPosition());
}
})
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.setExpectedPlayerEndedCount(2)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_IDLE,
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_ENDED);
testRunner.assertTimelinesSame(
placeholderTimeline,
timeline,
Timeline.EMPTY,
new FakeMediaSource.InitialTimeline(secondTimeline),
secondTimeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // stop(true)
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(mediaItemIndexAfterStop.get()).isEqualTo(1);
assertThat(positionAfterStop.get()).isAtLeast(1000L);
testRunner.assertPlayedPeriodIndices(0, 1);
}
@Test
public void resetPlaylistWithPreviousPosition() throws Exception {
Object firstWindowId = new Object();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId));
Timeline firstExpectedMaskingTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(firstWindowId).build());
Object secondWindowId = new Object();
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId));
Timeline secondExpectedMaskingTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(secondWindowId).build());
MediaSource secondSource = new FakeMediaSource(secondTimeline);
AtomicLong positionAfterReprepare = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 2000)
.setMediaSources(/* mediaItemIndex= */ 0, /* positionMs= */ 2000, secondSource)
.waitForTimelineChanged(
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionAfterReprepare.set(player.getCurrentPosition());
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(
firstExpectedMaskingTimeline, timeline, secondExpectedMaskingTimeline, secondTimeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(positionAfterReprepare.get()).isAtLeast(2000L);
}
@Test
public void resetPlaylistStartsFromDefaultPosition() throws Exception {
Object firstWindowId = new Object();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId));
Timeline firstExpectedPlaceholderTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(firstWindowId).build());
Object secondWindowId = new Object();
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId));
Timeline secondExpectedPlaceholderTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(secondWindowId).build());
MediaSource secondSource = new FakeMediaSource(secondTimeline);
AtomicLong positionAfterReprepare = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 2000)
.setMediaSources(/* resetPosition= */ true, secondSource)
.waitForTimelineChanged(
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionAfterReprepare.set(player.getCurrentPosition());
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(
firstExpectedPlaceholderTimeline,
timeline,
secondExpectedPlaceholderTimeline,
secondTimeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(positionAfterReprepare.get()).isEqualTo(0L);
}
@Test
public void resetPlaylistWithoutResettingPositionStartsFromOldPosition() throws Exception {
Object firstWindowId = new Object();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ firstWindowId));
Timeline firstExpectedPlaceholderTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(firstWindowId).build());
Object secondWindowId = new Object();
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ secondWindowId));
Timeline secondExpectedPlaceholderTimeline =
new MaskingMediaSource.PlaceholderTimeline(
FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(secondWindowId).build());
MediaSource secondSource = new FakeMediaSource(secondTimeline);
AtomicLong positionAfterReprepare = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 2000)
.setMediaSources(secondSource)
.waitForTimelineChanged(
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionAfterReprepare.set(player.getCurrentPosition());
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(
firstExpectedPlaceholderTimeline,
timeline,
secondExpectedPlaceholderTimeline,
secondTimeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertThat(positionAfterReprepare.get()).isAtLeast(2000L);
}
@Test
public void stopDuringPreparationOverwritesPreparation() throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.stop(true)
.waitForPendingPlayerCommands()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(new FakeTimeline())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(placeholderTimeline, Timeline.EMPTY);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}
@Test
public void stopAndSeekAfterStopDoesNotResetTimeline() throws Exception {
Timeline timeline = new FakeTimeline();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.stop(false)
.stop(false)
// Wait until the player fully processed the second stop to see that no further
// callbacks are triggered.
.waitForPendingPlayerCommands()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void reprepareAfterPlaybackError() throws Exception {
Timeline timeline = new FakeTimeline();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.prepare()
.waitForPlaybackState(Player.STATE_BUFFERING)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void seekAndReprepareAfterPlaybackError_keepsSeekPositionAndTimeline() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
FakeMediaSource fakeMediaSource = new FakeMediaSource();
player.setMediaSource(fakeMediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player
.createMessage(
(type, payload) -> {
throw ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
})
.send();
TestPlayerRunHelper.runUntilError(player);
player.seekTo(/* positionMs= */ 50);
runUntilPendingCommandsAreFullyHandled(player);
long positionAfterSeekHandled = player.getCurrentPosition();
// Delay re-preparation to force player to use its masking mechanisms.
fakeMediaSource.setAllowPreparation(false);
player.prepare();
runUntilPendingCommandsAreFullyHandled(player);
long positionAfterReprepareHandled = player.getCurrentPosition();
fakeMediaSource.setAllowPreparation(true);
runUntilPlaybackState(player, Player.STATE_READY);
long positionWhenFullyReadyAfterReprepare = player.getCurrentPosition();
player.release();
// Ensure we don't receive further timeline updates when repreparing.
verify(mockListener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
verify(mockListener).onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
verify(mockListener, times(2)).onTimelineChanged(any(), anyInt());
assertThat(positionAfterSeekHandled).isEqualTo(50);
assertThat(positionAfterReprepareHandled).isEqualTo(50);
assertThat(positionWhenFullyReadyAfterReprepare).isEqualTo(50);
}
@Test
public void restartAfterEmptyTimelineWithShuffleModeEnabledUsesCorrectFirstPeriod()
throws Exception {
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));
AtomicInteger mediaItemIndexAfterAddingSources = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.setShuffleModeEnabled(true)
// Preparing with an empty media source will transition to ended state.
.waitForPlaybackState(Player.STATE_ENDED)
// Add two sources at once such that the default start position in the shuffled order
// will be the second source.
.executeRunnable(
() ->
concatenatingMediaSource.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource())))
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndexAfterAddingSources.set(player.getCurrentMediaItemIndex());
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(mediaItemIndexAfterAddingSources.get()).isEqualTo(1);
}
@Test
public void playbackErrorAndReprepareDoesNotResetPosition() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
final long[] positionHolder = new long[3];
final int[] mediaItemIndexHolder = new int[3];
final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 500)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position while in error state
positionHolder[0] = player.getCurrentPosition();
mediaItemIndexHolder[0] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position while repreparing.
positionHolder[1] = player.getCurrentPosition();
mediaItemIndexHolder[1] = player.getCurrentMediaItemIndex();
}
})
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position after repreparation finished.
positionHolder[2] = player.getCurrentPosition();
mediaItemIndexHolder[2] = player.getCurrentMediaItemIndex();
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
assertThat(positionHolder[0]).isAtLeast(500L);
assertThat(positionHolder[1]).isEqualTo(positionHolder[0]);
assertThat(positionHolder[2]).isEqualTo(positionHolder[0]);
assertThat(mediaItemIndexHolder[0]).isEqualTo(1);
assertThat(mediaItemIndexHolder[1]).isEqualTo(1);
assertThat(mediaItemIndexHolder[2]).isEqualTo(1);
}
@Test
public void seekAfterPlaybackError() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
final long[] positionHolder = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final int[] mediaItemIndexHolder = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
final FakeMediaSource firstMediaSource = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 500)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position while in error state
positionHolder[0] = player.getCurrentPosition();
mediaItemIndexHolder[0] = player.getCurrentMediaItemIndex();
}
})
.seek(/* mediaItemIndex= */ 0, /* positionMs= */ C.TIME_UNSET)
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position while in error state
positionHolder[1] = player.getCurrentPosition();
mediaItemIndexHolder[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Position after prepare.
positionHolder[2] = player.getCurrentPosition();
mediaItemIndexHolder[2] = player.getCurrentMediaItemIndex();
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule)
.build();
assertThrows(
ExoPlaybackException.class,
() ->
testRunner
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS));
assertThat(positionHolder[0]).isAtLeast(500L);
assertThat(positionHolder[1]).isEqualTo(0L);
assertThat(positionHolder[2]).isEqualTo(0L);
assertThat(mediaItemIndexHolder[0]).isEqualTo(1);
assertThat(mediaItemIndexHolder[1]).isEqualTo(0);
assertThat(mediaItemIndexHolder[2]).isEqualTo(0);
}
@Test
public void playbackErrorAndReprepareWithPositionResetKeepsWindowSequenceNumber()
throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.seek(0, C.TIME_UNSET)
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.play()
.build();
HashSet<Long> reportedWindowSequenceNumbers = new HashSet<>();
AnalyticsListener listener =
new AnalyticsListener() {
@Override
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
if (eventTime.mediaPeriodId != null) {
reportedWindowSequenceNumbers.add(eventTime.mediaPeriodId.windowSequenceNumber);
}
}
@Override
public void onPlayWhenReadyChanged(
EventTime eventTime,
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int reason) {
if (eventTime.mediaPeriodId != null) {
reportedWindowSequenceNumbers.add(eventTime.mediaPeriodId.windowSequenceNumber);
}
}
};
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setAnalyticsListener(listener)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
assertThat(reportedWindowSequenceNumbers).hasSize(1);
}
@Test
public void playbackErrorTwiceStillKeepsTimeline() throws Exception {
final Timeline timeline = new FakeTimeline();
final FakeMediaSource mediaSource2 = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.setMediaSources(/* resetPosition= */ false, mediaSource2)
.prepare()
.waitForPlaybackState(Player.STATE_BUFFERING)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.waitForPlaybackState(Player.STATE_IDLE)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build();
try {
testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS);
fail();
} catch (ExoPlaybackException e) {
// Expected exception.
}
testRunner.assertTimelinesSame(placeholderTimeline, timeline, placeholderTimeline, timeline);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void sendMessagesDuringPreparation() throws Exception {
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline();
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void multipleSendMessages() throws Exception {
PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target80, /* positionMs= */ 80)
.sendMessage(target50, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target50.positionMs).isAtLeast(50L);
assertThat(target80.positionMs).isAtLeast(80L);
assertThat(target80.positionMs).isAtLeast(target50.positionMs);
}
@Test
public void sendMessagesFromStartPositionOnlyOnce() throws Exception {
AtomicInteger counter = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged()
.pause()
.sendMessage(
(messageType, payload) -> counter.getAndIncrement(),
/* mediaItemIndex= */ 0,
/* positionMs= */ 2000,
/* deleteAfterDelivery= */ false)
.seek(/* positionMs= */ 2000)
.delay(/* delayMs= */ 2000)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(counter.get()).isEqualTo(1);
}
@Test
public void multipleSendMessagesAtSameTime() throws Exception {
PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target1, /* positionMs= */ 50)
.sendMessage(target2, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target1.positionMs).isAtLeast(50L);
assertThat(target2.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesMultiPeriodResolution() throws Exception {
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0));
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesAtStartAndEndOfPeriod() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget targetEndMiddlePeriodResolved =
new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget targetEndMiddlePeriodUnresolved =
new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget targetEndLastPeriodResolved = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget targetEndLastPeriodUnresolved =
new PositionGrabbingMessageTarget();
long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs();
long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.sendMessage(targetStartFirstPeriod, /* mediaItemIndex= */ 0, /* positionMs= */ 0)
.sendMessage(
targetEndMiddlePeriodResolved,
/* mediaItemIndex= */ 0,
/* positionMs= */ duration1Ms - 1)
.sendMessage(
targetEndMiddlePeriodUnresolved,
/* mediaItemIndex= */ 0,
/* positionMs= */ C.TIME_END_OF_SOURCE)
.sendMessage(targetStartMiddlePeriod, /* mediaItemIndex= */ 1, /* positionMs= */ 0)
.sendMessage(
targetEndLastPeriodResolved,
/* mediaItemIndex= */ 1,
/* positionMs= */ duration2Ms - 1)
.sendMessage(
targetEndLastPeriodUnresolved,
/* mediaItemIndex= */ 1,
/* positionMs= */ C.TIME_END_OF_SOURCE)
.waitForMessage(targetEndLastPeriodUnresolved)
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(targetStartFirstPeriod.mediaItemIndex).isEqualTo(0);
assertThat(targetStartFirstPeriod.positionMs).isAtLeast(0L);
assertThat(targetEndMiddlePeriodResolved.mediaItemIndex).isEqualTo(0);
assertThat(targetEndMiddlePeriodResolved.positionMs).isAtLeast(duration1Ms - 1);
assertThat(targetEndMiddlePeriodUnresolved.mediaItemIndex).isEqualTo(0);
assertThat(targetEndMiddlePeriodUnresolved.positionMs).isAtLeast(duration1Ms - 1);
assertThat(targetEndMiddlePeriodResolved.positionMs)
.isEqualTo(targetEndMiddlePeriodUnresolved.positionMs);
assertThat(targetStartMiddlePeriod.mediaItemIndex).isEqualTo(1);
assertThat(targetStartMiddlePeriod.positionMs).isAtLeast(0L);
assertThat(targetEndLastPeriodResolved.mediaItemIndex).isEqualTo(1);
assertThat(targetEndLastPeriodResolved.positionMs).isAtLeast(duration2Ms - 1);
assertThat(targetEndLastPeriodUnresolved.mediaItemIndex).isEqualTo(1);
assertThat(targetEndLastPeriodUnresolved.positionMs).isAtLeast(duration2Ms - 1);
assertThat(targetEndLastPeriodResolved.positionMs)
.isEqualTo(targetEndLastPeriodUnresolved.positionMs);
}
@Test
public void sendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception {
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.seek(/* positionMs= */ 50)
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline();
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* positionMs= */ 50)
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception {
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.seek(/* positionMs= */ 51)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);
}
@Test
public void sendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline();
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.sendMessage(target, /* positionMs= */ 50)
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* positionMs= */ 51)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);
}
@Test
public void sendMessagesRepeatDoesNotRepost() throws Exception {
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(target, /* positionMs= */ 50)
.setRepeatMode(Player.REPEAT_MODE_ALL)
.play()
.waitForPositionDiscontinuity()
.setRepeatMode(Player.REPEAT_MODE_OFF)
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.messageCount).isEqualTo(1);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesRepeatWithoutDeletingDoesRepost() throws Exception {
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.sendMessage(
target,
/* mediaItemIndex= */ 0,
/* positionMs= */ 50,
/* deleteAfterDelivery= */ false)
.setRepeatMode(Player.REPEAT_MODE_ALL)
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 1)
.playUntilStartOfMediaItem(/* mediaItemIndex= */ 0)
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.messageCount).isEqualTo(2);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesMoveCurrentMediaItemIndex() throws Exception {
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));
final Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* positionMs= */ 50)
.executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline))
.waitForTimelineChanged(
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
assertThat(target.mediaItemIndex).isEqualTo(1);
}
@Test
public void sendMessagesMultiWindowDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* mediaItemIndex = */ 2, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.mediaItemIndex).isEqualTo(2);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesMultiWindowAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* mediaItemIndex = */ 2, /* positionMs= */ 50)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.mediaItemIndex).isEqualTo(2);
assertThat(target.positionMs).isAtLeast(50L);
}
@Test
public void sendMessagesMoveMediaItemIndex() throws Exception {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1));
final Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForTimelineChanged(
timeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.sendMessage(target, /* mediaItemIndex = */ 1, /* positionMs= */ 50)
.executeRunnable(() -> mediaSource.setNewSourceInfo(secondTimeline))
.waitForTimelineChanged(
secondTimeline, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.seek(/* mediaItemIndex= */ 0, /* positionMs= */ 0)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isAtLeast(50L);
assertThat(target.mediaItemIndex).isEqualTo(0);
}
@Test
public void sendMessagesNonLinearPeriodOrder() throws Exception {
Timeline fakeTimeline = new FakeTimeline();
MediaSource[] fakeMediaSources = {
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT)
};
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources);
PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget();
PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.sendMessage(target1, /* mediaItemIndex = */ 0, /* positionMs= */ 50)
.sendMessage(target2, /* mediaItemIndex = */ 1, /* positionMs= */ 50)
.sendMessage(target3, /* mediaItemIndex = */ 2, /* positionMs= */ 50)
.setShuffleModeEnabled(true)
.seek(/* mediaItemIndex= */ 2, /* positionMs= */ 0)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(target1.mediaItemIndex).isEqualTo(0);
assertThat(target2.mediaItemIndex).isEqualTo(1);
assertThat(target3.mediaItemIndex).isEqualTo(2);
}
@Test
public void cancelMessageBeforeDelivery() throws Exception {
final PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
final AtomicReference<PlayerMessage> message = new AtomicReference<>();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
message.set(
player.createMessage(target).setPosition(/* positionMs= */ 50).send());
}
})
// Play a bit to ensure message arrived in internal player.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 30)
.executeRunnable(() -> message.get().cancel())
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(message.get().isCanceled()).isTrue();
assertThat(target.messageCount).isEqualTo(0);
}
@Test
public void cancelRepeatedMessageAfterDelivery() throws Exception {
final CountingMessageTarget target = new CountingMessageTarget();
final AtomicReference<PlayerMessage> message = new AtomicReference<>();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
message.set(
player
.createMessage(target)
.setPosition(/* positionMs= */ 50)
.setDeleteAfterDelivery(/* deleteAfterDelivery= */ false)
.send());
}
})
// Play until the message has been delivered.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 51)
// Seek back, cancel the message, and play past the same position again.
.seek(/* positionMs= */ 0)
.executeRunnable(() -> message.get().cancel())
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(message.get().isCanceled()).isTrue();
assertThat(target.messageCount).isEqualTo(1);
}
@Test
public void sendMessages_withMediaRemoval_triggersCorrectMessagesAndDoesNotThrow()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
player
.createMessage((messageType, payload) -> {})
.setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 0)
.setDeleteAfterDelivery(false)
.send();
PlayerMessage.Target secondMediaItemTarget = mock(PlayerMessage.Target.class);
player
.createMessage(secondMediaItemTarget)
.setPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 0)
.setDeleteAfterDelivery(false)
.send();
// Play through media once to trigger all messages. This ensures any internally saved message
// indices are non-zero.
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
verify(secondMediaItemTarget).handleMessage(anyInt(), any());
// Remove first item and play second item again to check if message is triggered again.
// After removal, any internally saved message indices are invalid and will throw
// IndexOutOfBoundsException if used without updating.
// See https://github.com/google/ExoPlayer/issues/7278.
player.removeMediaItem(/* index= */ 0);
player.seekTo(/* positionMs= */ 0);
runUntilPlaybackState(player, Player.STATE_ENDED);
assertThat(player.getPlayerError()).isNull();
verify(secondMediaItemTarget, times(2)).handleMessage(anyInt(), any());
}
@Test
public void setAndSwitchSurface() throws Exception {
final List<Integer> rendererMessages = new ArrayList<>();
Renderer videoRenderer =
new FakeRenderer(C.TRACK_TYPE_VIDEO) {
@Override
public void handleMessage(@MessageType int messageType, @Nullable Object message)
throws ExoPlaybackException {
super.handleMessage(messageType, message);
rendererMessages.add(messageType);
}
};
ActionSchedule actionSchedule = addSurfaceSwitch(new ActionSchedule.Builder(TAG)).build();
new ExoPlayerTestRunner.Builder(context)
.setRenderers(videoRenderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(Collections.frequency(rendererMessages, Renderer.MSG_SET_VIDEO_OUTPUT)).isEqualTo(2);
}
@Test
public void switchSurfaceOnEndedState() throws Exception {
ActionSchedule.Builder scheduleBuilder =
new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED);
ActionSchedule waitForEndedAndSwitchSchedule = addSurfaceSwitch(scheduleBuilder).build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(Timeline.EMPTY)
.setActionSchedule(waitForEndedAndSwitchSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void timelineUpdateDropsPrebufferedPeriods() throws Exception {
Timeline timeline1 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2));
final Timeline timeline2 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
// Ensure next period is pre-buffered by playing until end of first period.
.playUntilPosition(
/* mediaItemIndex= */ 0,
/* positionMs= */ Util.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US))
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2))
.waitForTimelineChanged(
timeline2, /* expectedReason */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPlayedPeriodIndices(0, 1);
// Assert that the second period was re-created from the new timeline.
assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3);
assertThat(mediaSource.getCreatedMediaPeriods().get(0).periodUid)
.isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 0));
assertThat(mediaSource.getCreatedMediaPeriods().get(1).periodUid)
.isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 1));
assertThat(mediaSource.getCreatedMediaPeriods().get(2).periodUid)
.isEqualTo(timeline2.getUidOfPeriod(/* periodIndex= */ 1));
assertThat(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber)
.isGreaterThan(mediaSource.getCreatedMediaPeriods().get(0).windowSequenceNumber);
assertThat(mediaSource.getCreatedMediaPeriods().get(2).windowSequenceNumber)
.isGreaterThan(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber);
}
@Test
public void timelineUpdateWithNewMidrollAdCuePoint_dropsPrebufferedPeriod() throws Exception {
Timeline timeline1 = new FakeTimeline(TimelineWindowDefinition.createPlaceholder(/* tag= */ 0));
AdPlaybackState adPlaybackStateWithMidroll =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1,
/* adGroupTimesUs...= */ TimelineWindowDefinition
.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ 5 * C.MICROS_PER_SECOND);
Timeline timeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000,
adPlaybackStateWithMidroll));
FakeMediaSource mediaSource = new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline2))
.waitForTimelineChanged(
timeline2, /* expectedReason= */ Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPlayedPeriodIndices(0);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_AUTO_TRANSITION, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(4);
assertThat(mediaSource.getCreatedMediaPeriods().get(0).nextAdGroupIndex)
.isEqualTo(C.INDEX_UNSET);
assertThat(mediaSource.getCreatedMediaPeriods().get(1).nextAdGroupIndex).isEqualTo(0);
assertThat(mediaSource.getCreatedMediaPeriods().get(2).adGroupIndex).isEqualTo(0);
assertThat(mediaSource.getCreatedMediaPeriods().get(3).adGroupIndex).isEqualTo(C.INDEX_UNSET);
}
@Test
public void seekPastBufferingMidroll_playsAdAndThenContentFromSeekPosition() throws Exception {
long adGroupWindowTimeMs = 1_000;
long seekPositionMs = 95_000;
long contentDurationMs = 100_000;
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1,
/* adGroupTimesUs...= */ TimelineWindowDefinition
.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ Util.msToUs(adGroupWindowTimeMs));
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(contentDurationMs),
adPlaybackState));
AtomicBoolean hasCreatedAdMediaPeriod = new AtomicBoolean();
FakeMediaSource mediaSource =
new FakeMediaSource(timeline) {
@Override
public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
if (id.adGroupIndex == 0) {
hasCreatedAdMediaPeriod.set(true);
}
return super.createPeriod(id, allocator, startPositionUs);
}
};
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaSource(mediaSource);
// Throw on the playback thread if the player position reaches a value that is just less than
// seek position. This ensures that playback stops and the assertion on the player position
// below fails, even if a long time passes between detecting the discontinuity and asserting.
player
.createMessage(
(messageType, payload) -> {
throw new IllegalStateException();
})
.setPosition(seekPositionMs - 1)
.send();
player.pause();
player.prepare();
// Block until the midroll has started buffering, then seek after the midroll before playing.
runMainLooperUntil(hasCreatedAdMediaPeriod::get);
player.seekTo(seekPositionMs);
player.play();
// When the ad finishes, the player position should be at or after the requested seek position.
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(player.getCurrentPosition()).isAtLeast(seekPositionMs);
}
@Test
public void repeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber()
throws Exception {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 2,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND));
FakeMediaSource mediaSource = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.seek(/* mediaItemIndex= */ 0, /* positionMs= */ 9999)
// Wait after each seek until the internal player has updated its state.
.waitForPendingPlayerCommands()
.seek(/* mediaItemIndex= */ 0, /* positionMs= */ 1)
.waitForPendingPlayerCommands()
.seek(/* mediaItemIndex= */ 0, /* positionMs= */ 9999)
.waitForPendingPlayerCommands()
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 1);
assertThat(mediaSource.getCreatedMediaPeriods())
.containsAtLeast(
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0),
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
assertThat(mediaSource.getCreatedMediaPeriods())
.doesNotContain(
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
}
@Test
public void invalidSeekFallsBackToSubsequentPeriodOfTheRemovedPeriod() throws Exception {
Timeline timeline = new FakeTimeline();
CountDownLatch sourceReleasedCountDownLatch = new CountDownLatch(/* count= */ 1);
MediaSource mediaSourceToRemove =
new FakeMediaSource(timeline) {
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
sourceReleasedCountDownLatch.countDown();
}
};
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(mediaSourceToRemove, new FakeMediaSource(timeline));
final int[] windowCount = {C.INDEX_UNSET};
final long[] position = {C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaSource.removeMediaSource(/* index= */ 0);
try {
// Wait until the source to be removed is released on the playback thread. So
// the timeline in EPII has one window only, but the update here in EPI is
// stuck until we finished our executable here. So when seeking below, we will
// seek in the timeline which still has two windows in EPI, but when the seek
// arrives in EPII the actual timeline has one window only. Hence it tries to
// find the subsequent period of the removed period and finds it.
player.getClock().onThreadBlocked();
sourceReleasedCountDownLatch.await();
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1000L);
}
})
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
windowCount[0] = player.getCurrentTimeline().getWindowCount();
position[0] = player.getCurrentPosition();
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect the first window to be the current.
assertThat(windowCount[0]).isEqualTo(1);
// Expect the position to be in the default position.
assertThat(position[0]).isEqualTo(0L);
}
@Test
public void onPlayerErrorChanged_isNotifiedForNullError() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(
new FakeMediaSource(/* timeline= */ null) {
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException();
}
});
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.prepare();
player.play();
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
// The media source fails preparation, so we expect both methods to be called.
verify(mockListener).onPlayerErrorChanged(error);
verify(mockListener).onPlayerError(error);
reset(mockListener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, STATE_ENDED);
// Now the player, which had a playback error, was re-prepared causing the error to be cleared.
// We expect the change to null to be notified, but not onPlayerError.
verify(mockListener).onPlayerErrorChanged(ArgumentMatchers.isNull());
verify(mockListener, never()).onPlayerError(any());
}
@Test
public void recursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception {
// We add two listeners to the player. The first stops the player as soon as it's ready and both
// record the state change events they receive.
final AtomicReference<Player> playerReference = new AtomicReference<>();
final List<Integer> playerListener1States = new ArrayList<>();
final List<Integer> playerListener2States = new ArrayList<>();
final Player.Listener playerListener1 =
new Player.Listener() {
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
playerListener1States.add(playbackState);
if (playbackState == Player.STATE_READY) {
playerReference.get().stop(/* reset= */ true);
}
}
};
final Player.Listener playerListener2 =
new Player.Listener() {
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
playerListener2States.add(playbackState);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener1);
player.addListener(playerListener2);
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(playerListener1States)
.containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)
.inOrder();
assertThat(playerListener2States)
.containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)
.inOrder();
}
@Test
public void recursivePlayerChangesAreReportedInCorrectOrder() throws Exception {
// The listener stops the player as soon as it's ready (which should report a timeline and state
// change) and sets playWhenReady to false when the timeline callback is received.
final AtomicReference<Player> playerReference = new AtomicReference<>();
final List<Boolean> playerListenerPlayWhenReady = new ArrayList<>();
final List<Integer> playerListenerStates = new ArrayList<>();
List<Integer> sequence = new ArrayList<>();
final Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
playerListenerStates.add(playbackState);
if (playbackState == Player.STATE_READY) {
playerReference.get().stop(/* reset= */ true);
sequence.add(0);
}
}
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
if (timeline.isEmpty()) {
playerReference.get().pause();
sequence.add(1);
}
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
playerListenerPlayWhenReady.add(playWhenReady);
sequence.add(2);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(playerListenerStates)
.containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE)
.inOrder();
assertThat(playerListenerPlayWhenReady).containsExactly(false).inOrder();
assertThat(sequence).containsExactly(0, 1, 2).inOrder();
}
@Test
public void recursiveTimelineChangeInStopAreReportedInCorrectOrder() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 3);
final AtomicReference<ExoPlayer> playerReference = new AtomicReference<>();
FakeMediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
final Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_IDLE) {
playerReference.get().setMediaSource(secondMediaSource);
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
// Ensure there are no further pending callbacks.
.delay(1)
.stop(/* reset= */ true)
.waitForPlaybackState(Player.STATE_IDLE)
.prepare()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setTimeline(firstTimeline)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertTimelinesSame(
new FakeMediaSource.InitialTimeline(firstTimeline),
firstTimeline,
Timeline.EMPTY,
new FakeMediaSource.InitialTimeline(secondTimeline),
secondTimeline);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void clippedLoopedPeriodsArePlayedFully() throws Exception {
long startPositionUs = 300_000;
long expectedDurationUs = 700_000;
MediaSource mediaSource =
new ClippingMediaSource(
new FakeMediaSource(), startPositionUs, startPositionUs + expectedDurationUs);
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong positionAtDiscontinuityMs = new AtomicLong(C.TIME_UNSET);
AtomicLong clockAtStartMs = new AtomicLong(C.TIME_UNSET);
AtomicLong clockAtDiscontinuityMs = new AtomicLong(C.TIME_UNSET);
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
if (playbackState == Player.STATE_READY && clockAtStartMs.get() == C.TIME_UNSET) {
clockAtStartMs.set(clock.elapsedRealtime());
}
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
positionAtDiscontinuityMs.set(playerReference.get().getCurrentPosition());
clockAtDiscontinuityMs.set(clock.elapsedRealtime());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
.pause()
.setRepeatMode(Player.REPEAT_MODE_ALL)
.waitForPlaybackState(Player.STATE_READY)
// Play until the media repeats once.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 1)
.playUntilStartOfMediaItem(/* mediaItemIndex= */ 0)
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setClock(clock)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionAtDiscontinuityMs.get()).isAtLeast(0L);
assertThat(clockAtDiscontinuityMs.get() - clockAtStartMs.get())
.isAtLeast(Util.usToMs(expectedDurationUs));
}
@Test
public void updateTrackSelectorThenSeekToUnpreparedPeriod_returnsEmptyTrackGroups()
throws Exception {
// Use unset duration to prevent pre-loading of the second window.
Timeline timelineUnsetDuration =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ C.TIME_UNSET));
Timeline timelineSetDuration = new FakeTimeline();
MediaSource mediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(timelineUnsetDuration, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(timelineSetDuration, ExoPlayerTestRunner.AUDIO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0)
.waitForPendingPlayerCommands()
.play()
.build();
List<TrackGroupArray> trackGroupsList = new ArrayList<>();
List<TrackSelectionArray> trackSelectionsList = new ArrayList<>();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setSupportedFormats(ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT)
.setActionSchedule(actionSchedule)
.setPlayerListener(
new Player.Listener() {
@Override
public void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
trackGroupsList.add(trackGroups);
trackSelectionsList.add(trackSelections);
}
})
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(trackGroupsList).hasSize(3);
// First track groups of the 1st period are reported.
// Then the seek to an unprepared period will result in empty track groups and selections being
// returned.
// Then the track groups of the 2nd period are reported.
assertThat(trackGroupsList.get(0).get(0).getFormat(0))
.isEqualTo(ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(trackGroupsList.get(1)).isEqualTo(TrackGroupArray.EMPTY);
assertThat(trackSelectionsList.get(1).get(0)).isNull();
assertThat(trackGroupsList.get(2).get(0).getFormat(0))
.isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT);
}
@Test
public void removingLoopingLastPeriodFromPlaylistDoesNotThrow() throws Exception {
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 100_000));
MediaSource mediaSource = new FakeMediaSource(timeline);
ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(mediaSource);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
// Play almost to end to ensure the current period is fully buffered.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 90)
// Enable repeat mode to trigger the creation of new media periods.
.setRepeatMode(Player.REPEAT_MODE_ALL)
// Remove the media source.
.executeRunnable(concatenatingMediaSource::clear)
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void seekToUnpreparedWindowWithNonZeroOffsetInConcatenationStartsAtCorrectPosition()
throws Exception {
FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null);
MediaSource clippedMediaSource =
new ClippingMediaSource(
mediaSource,
/* startPositionUs= */ 3 * C.MICROS_PER_SECOND,
/* endPositionUs= */ C.TIME_END_OF_SOURCE);
MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(clippedMediaSource);
AtomicLong positionWhenReady = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
// Seek while unprepared and wait until the player handled all updates.
.seek(/* positionMs= */ 10)
.waitForPendingPlayerCommands()
// Finish preparation.
.executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline()))
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionWhenReady.set(player.getContentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatedMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionWhenReady.get()).isEqualTo(10);
}
@Test
public void seekToUnpreparedWindowWithMultiplePeriodsInConcatenationStartsAtCorrectPeriod()
throws Exception {
long periodDurationMs = 5000;
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 2,
/* id= */ new Object(),
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 2 * periodDurationMs * 1000));
FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null);
MediaSource concatenatedMediaSource = new ConcatenatingMediaSource(mediaSource);
AtomicInteger periodIndexWhenReady = new AtomicInteger();
AtomicLong positionWhenReady = new AtomicLong();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
// Seek 10ms into the second period.
.seek(/* positionMs= */ periodDurationMs + 10)
.executeRunnable(() -> mediaSource.setNewSourceInfo(timeline))
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
periodIndexWhenReady.set(player.getCurrentPeriodIndex());
positionWhenReady.set(player.getContentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatedMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(periodIndexWhenReady.get()).isEqualTo(1);
assertThat(positionWhenReady.get()).isEqualTo(periodDurationMs + 10);
}
@Test
public void periodTransitionReportsCorrectBufferedPosition() throws Exception {
int periodCount = 3;
long periodDurationUs = 5 * C.MICROS_PER_SECOND;
long windowDurationUs = periodCount * periodDurationUs;
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
periodCount,
/* id= */ new Object(),
/* isSeekable= */ true,
/* isDynamic= */ false,
windowDurationUs));
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong bufferedPositionAtFirstDiscontinuityMs = new AtomicLong(C.TIME_UNSET);
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (bufferedPositionAtFirstDiscontinuityMs.get() == C.TIME_UNSET) {
bufferedPositionAtFirstDiscontinuityMs.set(
playerReference.get().getBufferedPosition());
}
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
.pause()
// Wait until all periods are fully buffered.
.waitForIsLoading(/* targetIsLoading= */ true)
.waitForIsLoading(/* targetIsLoading= */ false)
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(bufferedPositionAtFirstDiscontinuityMs.get())
.isEqualTo(Util.usToMs(windowDurationUs));
}
@Test
public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000,
adPlaybackState));
FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
.seek(/* positionMs= */ 5_000)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline))
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);
}
@Test
public void contentWithoutInitialSeekStartsAtDefaultPositionAfterPrerollAd() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 5_000_000,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
adPlaybackState));
FakeMediaSource fakeMediaSource = new FakeMediaSource(/* timeline= */ null);
AtomicReference<Player> playerReference = new AtomicReference<>();
AtomicLong contentStartPositionMs = new AtomicLong(C.TIME_UNSET);
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerReference.set(player);
player.addListener(playerListener);
}
})
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(() -> fakeMediaSource.setNewSourceInfo(fakeTimeline))
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(fakeMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(contentStartPositionMs.get()).isAtLeast(5_000L);
}
@Test
public void adInMovingLiveWindow_keepsContentPosition() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 42_000_004_000_000L);
Timeline liveTimeline1 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 3_000_000,
/* windowOffsetInFirstPeriodUs= */ 42_000_000_000_000L,
adPlaybackState));
Timeline liveTimeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 3_000_000,
/* windowOffsetInFirstPeriodUs= */ 42_000_002_000_000L,
adPlaybackState));
FakeMediaSource fakeMediaSource = new FakeMediaSource(liveTimeline1);
player.setMediaSource(fakeMediaSource);
player.prepare();
player.play();
// Wait until the ad is playing.
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
long contentPositionBeforeLiveWindowUpdateMs = player.getContentPosition();
fakeMediaSource.setNewSourceInfo(liveTimeline2);
runUntilTimelineChanged(player);
long contentPositionAfterLiveWindowUpdateMs = player.getContentPosition();
player.release();
assertThat(contentPositionBeforeLiveWindowUpdateMs).isEqualTo(4000);
assertThat(contentPositionAfterLiveWindowUpdateMs).isEqualTo(2000);
}
@Test
public void setPlaybackSpeedConsecutivelyNotifiesListenerForEveryChangeOnceAndIsMasked()
throws Exception {
List<Float> maskedPlaybackSpeeds = new ArrayList<>();
Action getPlaybackSpeedAction =
new Action("getPlaybackSpeed", /* description= */ null) {
@Override
protected void doActionImpl(
ExoPlayer player, DefaultTrackSelector trackSelector, @Nullable Surface surface) {
maskedPlaybackSpeeds.add(player.getPlaybackParameters().speed);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
.apply(getPlaybackSpeedAction)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
.apply(getPlaybackSpeedAction)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
.apply(getPlaybackSpeedAction)
.play()
.build();
List<Float> reportedPlaybackSpeeds = new ArrayList<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
reportedPlaybackSpeeds.add(playbackParameters.speed);
}
};
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setPlayerListener(listener)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(reportedPlaybackSpeeds).containsExactly(1.1f, 1.2f, 1.3f).inOrder();
assertThat(maskedPlaybackSpeeds).isEqualTo(reportedPlaybackSpeeds);
}
@Test
public void
setUnsupportedPlaybackSpeedConsecutivelyNotifiesListenerForEveryChangeOnceAndResetsOnceHandled()
throws Exception {
Renderer renderer =
new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) {
@Override
public long getPositionUs() {
return 0;
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {}
@Override
public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT;
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.1f))
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.2f))
.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.3f))
.play()
.build();
List<PlaybackParameters> reportedPlaybackParameters = new ArrayList<>();
Player.Listener listener =
new Player.Listener() {
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
reportedPlaybackParameters.add(playbackParameters);
}
};
new ExoPlayerTestRunner.Builder(context)
.setSupportedFormats(ExoPlayerTestRunner.AUDIO_FORMAT)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.setPlayerListener(listener)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(reportedPlaybackParameters)
.containsExactly(
new PlaybackParameters(/* speed= */ 1.1f),
new PlaybackParameters(/* speed= */ 1.2f),
new PlaybackParameters(/* speed= */ 1.3f),
PlaybackParameters.DEFAULT)
.inOrder();
}
@Test
public void simplePlaybackHasNoPlaybackSuppression() throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.play()
.waitForPlaybackState(Player.STATE_READY)
.pause()
.play()
.build();
AtomicBoolean seenPlaybackSuppression = new AtomicBoolean();
Player.Listener listener =
new Player.Listener() {
@Override
public void onPlaybackSuppressionReasonChanged(
@Player.PlaybackSuppressionReason int playbackSuppressionReason) {
seenPlaybackSuppression.set(true);
}
};
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setPlayerListener(listener)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(seenPlaybackSuppression.get()).isFalse();
}
@Test
public void audioFocusDenied() throws Exception {
ShadowAudioManager shadowAudioManager = shadowOf(context.getSystemService(AudioManager.class));
shadowAudioManager.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_FAILED);
PlayerStateGrabber playerStateGrabber = new PlayerStateGrabber();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true)
.play()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(playerStateGrabber)
.build();
AtomicBoolean seenPlaybackSuppression = new AtomicBoolean();
Player.Listener listener =
new Player.Listener() {
@Override
public void onPlaybackSuppressionReasonChanged(
@Player.PlaybackSuppressionReason int playbackSuppressionReason) {
seenPlaybackSuppression.set(true);
}
};
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.setPlayerListener(listener)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS);
assertThat(playerStateGrabber.playWhenReady).isFalse();
assertThat(seenPlaybackSuppression.get()).isFalse();
}
@Test
public void delegatingMediaSourceApproach() throws Exception {
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000_000));
final ConcatenatingMediaSource underlyingSource = new ConcatenatingMediaSource();
CompositeMediaSource<Void> delegatingMediaSource =
new CompositeMediaSource<Void>() {
@Override
public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
underlyingSource.addMediaSource(
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT));
underlyingSource.addMediaSource(
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT));
prepareChildSource(null, underlyingSource);
}
@Override
public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
return underlyingSource.createPeriod(id, allocator, startPositionUs);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
underlyingSource.releasePeriod(mediaPeriod);
}
@Override
protected void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline timeline) {
refreshSourceInfo(timeline);
}
@Override
public boolean isSingleWindow() {
return false;
}
@Override
public MediaItem getMediaItem() {
return underlyingSource.getMediaItem();
}
@Override
@Nullable
public Timeline getInitialTimeline() {
return Timeline.EMPTY;
}
};
int[] currentMediaItemIndices = new int[1];
long[] currentPlaybackPositions = new long[1];
long[] windowCounts = new long[1];
int seekToMediaItemIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 5000)
.waitForTimelineChanged(
/* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[0] = player.getCurrentPosition();
windowCounts[0] = player.getCurrentTimeline().getWindowCount();
}
})
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(delegatingMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
assertArrayEquals(new long[] {2}, windowCounts);
assertArrayEquals(new int[] {seekToMediaItemIndex}, currentMediaItemIndices);
assertArrayEquals(new long[] {5_000}, currentPlaybackPositions);
}
@SuppressWarnings("deprecation")
@Test
public void seekTo_mediaItemIndexIsReset_deprecated() throws Exception {
FakeTimeline fakeTimeline = new FakeTimeline();
FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline);
final int[] mediaItemIndex = {C.INDEX_UNSET};
final long[] positionMs = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.playUntilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 3000)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionMs[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
//noinspection deprecation
player.prepare(mediaSource);
player.seekTo(/* positionMs= */ 7000);
positionMs[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[0] = player.getCurrentMediaItemIndex();
positionMs[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS);
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(3000L);
assertThat(positionMs[1]).isEqualTo(7000L);
assertThat(positionMs[2]).isEqualTo(7000L);
assertThat(bufferedPositions[0]).isAtLeast(3000L);
assertThat(bufferedPositions[1]).isEqualTo(7000L);
assertThat(bufferedPositions[2])
.isEqualTo(fakeTimeline.getWindow(0, new Window()).getDurationMs());
}
@Test
public void seekTo_mediaItemIndexIsReset() throws Exception {
FakeTimeline fakeTimeline = new FakeTimeline();
FakeMediaSource mediaSource = new FakeMediaSource(fakeTimeline);
final int[] mediaItemIndex = {C.INDEX_UNSET};
final long[] positionMs = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.playUntilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 3000)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionMs[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
player.setMediaSource(mediaSource, /* startPositionMs= */ 7000);
player.prepare();
positionMs[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[0] = player.getCurrentMediaItemIndex();
positionMs[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS);
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(3000);
assertThat(positionMs[1]).isEqualTo(7000);
assertThat(positionMs[2]).isEqualTo(7000);
assertThat(bufferedPositions[0]).isAtLeast(3000);
assertThat(bufferedPositions[1]).isEqualTo(7000);
assertThat(bufferedPositions[2])
.isEqualTo(fakeTimeline.getWindow(0, new Window()).getDurationMs());
}
@Test
public void seekTo_singlePeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(9000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(9000);
assertThat(bufferedPositions[0]).isEqualTo(9200);
assertThat(totalBufferedDuration[0]).isEqualTo(200);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(9200);
assertThat(totalBufferedDuration[1]).isEqualTo(200);
}
@Test
public void seekTo_singlePeriod_beyondBufferedData_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(9200);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(9200);
assertThat(bufferedPositions[0]).isEqualTo(9200);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(9200);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
}
@Test
public void seekTo_backwardsSinglePeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(1000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(1000);
assertThat(bufferedPositions[0]).isEqualTo(1000);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
}
@Test
public void seekTo_backwardsMultiplePeriods_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(0, 1000);
}
},
/* pauseMediaItemIndex= */ 1,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 9200));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(1000);
assertThat(bufferedPositions[0]).isEqualTo(1000);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
}
@Test
public void seekTo_toUnbufferedPeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(2, 1000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0));
assertThat(mediaItemIndex[0]).isEqualTo(2);
assertThat(positionMs[0]).isEqualTo(1000);
assertThat(bufferedPositions[0]).isEqualTo(1000);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
assertThat(mediaItemIndex[1]).isEqualTo(2);
assertThat(positionMs[1]).isEqualTo(1000);
assertThat(bufferedPositions[1]).isEqualTo(1000);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
}
@Test
public void seekTo_toLoadingPeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(1, 1000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource());
assertThat(mediaItemIndex[0]).isEqualTo(1);
assertThat(positionMs[0]).isEqualTo(1000);
// TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully
// covered.
// assertThat(bufferedPositions[0]).isEqualTo(10_000);
// assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void seekTo_toLoadingPeriod_withinPartiallyBufferedData_correctMaskingPosition()
throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(1, 1000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(1);
assertThat(positionMs[0]).isEqualTo(1000);
// TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully
// covered.
// assertThat(bufferedPositions[0]).isEqualTo(1000);
// assertThat(totalBufferedDuration[0]).isEqualTo(0);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(4000);
assertThat(totalBufferedDuration[1]).isEqualTo(3000);
}
@Test
public void seekTo_toLoadingPeriod_beyondBufferedData_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(1, 5000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(1);
assertThat(positionMs[0]).isEqualTo(5000);
assertThat(bufferedPositions[0]).isEqualTo(5000);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
assertThat(mediaItemIndex[1]).isEqualTo(1);
assertThat(positionMs[1]).isEqualTo(5000);
assertThat(bufferedPositions[1]).isEqualTo(5000);
assertThat(totalBufferedDuration[1]).isEqualTo(0);
}
@Test
public void seekTo_toInnerFullyBufferedPeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(1, 5000);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(1);
assertThat(positionMs[0]).isEqualTo(5000);
// TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully
// covered.
// assertThat(bufferedPositions[0]).isEqualTo(10_000);
// assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void addMediaSource_withinBufferedPeriods_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addMediaSource(
/* index= */ 1, createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0));
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void moveMediaItem_behindLoadingPeriod_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 2);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void moveMediaItem_undloadedBehindPlaying_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.moveMediaItem(/* currentIndex= */ 3, /* newIndex= */ 1);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 0));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void removeMediaItem_removePlayingWindow_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.removeMediaItem(/* index= */ 0);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(0);
// TODO(b/160450903): Verify masking of buffering properties when behaviour in EPII is fully
// covered.
// assertThat(bufferedPositions[0]).isEqualTo(4000);
// assertThat(totalBufferedDuration[0]).isEqualTo(4000);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(4000);
assertThat(totalBufferedDuration[1]).isEqualTo(4000);
}
@Test
public void removeMediaItem_removeLoadingWindow_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.removeMediaItem(/* index= */ 2);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isAtLeast(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[1]);
}
@Test
public void removeMediaItem_removeInnerFullyBufferedWindow_correctMaskingPosition()
throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.removeMediaItem(/* index= */ 1);
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(8000);
assertThat(bufferedPositions[0]).isEqualTo(10_000);
assertThat(totalBufferedDuration[0]).isEqualTo(10_000 - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(0);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(10_000);
assertThat(totalBufferedDuration[1]).isEqualTo(10_000 - positionMs[0]);
}
@Test
public void clearMediaItems_correctMaskingPosition() throws Exception {
final int[] mediaItemIndex = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] positionMs = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] bufferedPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] totalBufferedDuration = {C.INDEX_UNSET, C.INDEX_UNSET};
runPositionMaskingCapturingActionSchedule(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.clearMediaItems();
}
},
/* pauseMediaItemIndex= */ 0,
mediaItemIndex,
positionMs,
bufferedPositions,
totalBufferedDuration,
new FakeMediaSource(),
new FakeMediaSource(),
createPartiallyBufferedMediaSource(/* maxBufferedPositionMs= */ 4000));
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(positionMs[0]).isEqualTo(0);
assertThat(bufferedPositions[0]).isEqualTo(0);
assertThat(totalBufferedDuration[0]).isEqualTo(0);
assertThat(mediaItemIndex[1]).isEqualTo(mediaItemIndex[0]);
assertThat(positionMs[1]).isEqualTo(positionMs[0]);
assertThat(bufferedPositions[1]).isEqualTo(bufferedPositions[0]);
assertThat(totalBufferedDuration[1]).isEqualTo(totalBufferedDuration[0]);
}
private void runPositionMaskingCapturingActionSchedule(
PlayerRunnable actionRunnable,
int pauseMediaItemIndex,
int[] mediaItemIndex,
long[] positionMs,
long[] bufferedPosition,
long[] totalBufferedDuration,
MediaSource... mediaSources)
throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.playUntilPosition(pauseMediaItemIndex, /* positionMs= */ 8000)
.executeRunnable(actionRunnable)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[0] = player.getCurrentMediaItemIndex();
positionMs[0] = player.getCurrentPosition();
bufferedPosition[0] = player.getBufferedPosition();
totalBufferedDuration[0] = player.getTotalBufferedDuration();
}
})
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[1] = player.getCurrentMediaItemIndex();
positionMs[1] = player.getCurrentPosition();
bufferedPosition[1] = player.getBufferedPosition();
totalBufferedDuration[1] = player.getTotalBufferedDuration();
}
})
.stop()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSources)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
}
private static FakeMediaSource createPartiallyBufferedMediaSource(long maxBufferedPositionMs) {
int windowOffsetInFirstPeriodUs = 1_000_000;
FakeTimeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ false,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000L,
/* defaultPositionUs= */ 0,
windowOffsetInFirstPeriodUs,
AdPlaybackState.NONE));
return new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
/* trackDataFactory= */ (format, mediaPeriodId) ->
ImmutableList.of(
oneByteSample(windowOffsetInFirstPeriodUs, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(
windowOffsetInFirstPeriodUs + Util.msToUs(maxBufferedPositionMs),
C.BUFFER_FLAG_KEY_FRAME)),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false);
}
};
}
@Test
public void addMediaSource_whilePlayingAd_correctMasking() throws Exception {
long contentDurationMs = 10_000;
long adDurationMs = 100_000;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0);
adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
adPlaybackState =
adPlaybackState.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY);
long[][] durationsUs = new long[1][];
durationsUs[0] = new long[] {Util.msToUs(adDurationMs)};
adPlaybackState = adPlaybackState.withAdDurationsUs(durationsUs);
Timeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(contentDurationMs),
adPlaybackState));
FakeMediaSource adsMediaSource = new FakeMediaSource(adTimeline);
int[] mediaItemIndex = new int[] {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
long[] positionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET};
long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET};
long[] totalBufferedDurationMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.INDEX_UNSET};
boolean[] isPlayingAd = new boolean[3];
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForIsLoading(true)
.waitForIsLoading(false)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addMediaSource(/* index= */ 1, new FakeMediaSource());
mediaItemIndex[0] = player.getCurrentMediaItemIndex();
isPlayingAd[0] = player.isPlayingAd();
positionMs[0] = player.getCurrentPosition();
bufferedPositionMs[0] = player.getBufferedPosition();
totalBufferedDurationMs[0] = player.getTotalBufferedDuration();
}
})
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[1] = player.getCurrentMediaItemIndex();
isPlayingAd[1] = player.isPlayingAd();
positionMs[1] = player.getCurrentPosition();
bufferedPositionMs[1] = player.getBufferedPosition();
totalBufferedDurationMs[1] = player.getTotalBufferedDuration();
}
})
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8000)
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addMediaSource(new FakeMediaSource());
mediaItemIndex[2] = player.getCurrentMediaItemIndex();
isPlayingAd[2] = player.isPlayingAd();
positionMs[2] = player.getCurrentPosition();
bufferedPositionMs[2] = player.getBufferedPosition();
totalBufferedDurationMs[2] = player.getTotalBufferedDuration();
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(adsMediaSource, new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(isPlayingAd[0]).isTrue();
assertThat(positionMs[0]).isAtMost(adDurationMs);
assertThat(bufferedPositionMs[0]).isEqualTo(adDurationMs);
assertThat(totalBufferedDurationMs[0]).isAtLeast(adDurationMs - positionMs[0]);
assertThat(mediaItemIndex[1]).isEqualTo(0);
assertThat(isPlayingAd[1]).isTrue();
assertThat(positionMs[1]).isAtMost(adDurationMs);
assertThat(bufferedPositionMs[1]).isEqualTo(adDurationMs);
assertThat(totalBufferedDurationMs[1]).isAtLeast(adDurationMs - positionMs[1]);
assertThat(mediaItemIndex[2]).isEqualTo(0);
assertThat(isPlayingAd[2]).isFalse();
assertThat(positionMs[2]).isEqualTo(8000);
assertThat(bufferedPositionMs[2]).isEqualTo(contentDurationMs);
assertThat(totalBufferedDurationMs[2]).isAtLeast(contentDurationMs - positionMs[2]);
}
@Test
public void seekTo_whilePlayingAd_correctMasking() throws Exception {
long contentDurationMs = 10_000;
long adDurationMs = 4_000;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0);
adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1);
adPlaybackState =
adPlaybackState.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY);
long[][] durationsUs = new long[1][];
durationsUs[0] = new long[] {Util.msToUs(adDurationMs)};
adPlaybackState = adPlaybackState.withAdDurationsUs(durationsUs);
Timeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(contentDurationMs),
adPlaybackState));
FakeMediaSource adsMediaSource = new FakeMediaSource(adTimeline);
int[] mediaItemIndex = new int[] {C.INDEX_UNSET, C.INDEX_UNSET};
long[] positionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET};
long[] totalBufferedDurationMs = new long[] {C.TIME_UNSET, C.TIME_UNSET};
boolean[] isPlayingAd = new boolean[2];
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.waitForIsLoading(true)
.waitForIsLoading(false)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 8000);
mediaItemIndex[0] = player.getCurrentMediaItemIndex();
isPlayingAd[0] = player.isPlayingAd();
positionMs[0] = player.getCurrentPosition();
bufferedPositionMs[0] = player.getBufferedPosition();
totalBufferedDurationMs[0] = player.getTotalBufferedDuration();
}
})
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndex[1] = player.getCurrentMediaItemIndex();
isPlayingAd[1] = player.isPlayingAd();
positionMs[1] = player.getCurrentPosition();
bufferedPositionMs[1] = player.getBufferedPosition();
totalBufferedDurationMs[1] = player.getTotalBufferedDuration();
}
})
.stop()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(adsMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(mediaItemIndex[0]).isEqualTo(0);
assertThat(isPlayingAd[0]).isTrue();
assertThat(positionMs[0]).isEqualTo(0);
assertThat(bufferedPositionMs[0]).isEqualTo(adDurationMs);
assertThat(totalBufferedDurationMs[0]).isEqualTo(adDurationMs);
assertThat(mediaItemIndex[1]).isEqualTo(0);
assertThat(isPlayingAd[1]).isTrue();
assertThat(positionMs[1]).isEqualTo(0);
assertThat(bufferedPositionMs[1]).isEqualTo(adDurationMs);
assertThat(totalBufferedDurationMs[1]).isEqualTo(adDurationMs);
}
// https://github.com/google/ExoPlayer/issues/8349
@Test
public void seekTo_whilePlayingAd_doesntBlockFutureUpdates() throws Exception {
long contentDurationMs = 10_000;
long adDurationMs = 4_000;
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY);
long[][] durationsUs = new long[1][];
durationsUs[0] = new long[] {Util.msToUs(adDurationMs)};
adPlaybackState = adPlaybackState.withAdDurationsUs(durationsUs);
Timeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(contentDurationMs),
adPlaybackState));
FakeMediaSource adsMediaSource = new FakeMediaSource(adTimeline);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaSource(adsMediaSource);
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(0, 8000);
player.play();
// This times out if playback info updates after the seek are blocked.
runUntilPlaybackState(player, Player.STATE_ENDED);
}
@Test
public void seekTo_beyondSSAIMidRolls_seekAdjustedAndRequestedContentPositionKept()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* positionMs= */ 4000);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// seek discontinuities
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(3);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(4000);
// seek adjustment
assertThat(oldPositions.get(1).periodIndex).isEqualTo(3);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(4000);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(4000);
// auto transition from ad to end of period
assertThat(oldPositions.get(2).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(2500);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(4000);
assertThat(newPositions.get(2).periodIndex).isEqualTo(1);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).positionMs).isEqualTo(2500);
// auto transition to next ad period
assertThat(oldPositions.get(3).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(3).periodIndex).isEqualTo(2);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(3).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(4000);
// auto transition from ad to end of period
assertThat(oldPositions.get(4).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(4).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(4).periodIndex).isEqualTo(2);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
// auto transition to final content period with seek position
assertThat(oldPositions.get(5).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(4000);
}
@Test
public void seekTo_beyondSSAIMidRollsConsecutiveContentPeriods_seekAdjusted() throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
false,
false);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* positionMs= */ 7000);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(5))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// seek
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(3);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(7000);
// seek adjustment
assertThat(oldPositions.get(1).periodIndex).isEqualTo(3);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(7000);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
}
@Test
public void seekTo_beforeSSAIMidRolls_requestedContentPositionNotPropagatedIntoAds()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.play();
player.seekTo(1600);
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 0, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(newPositions.get(0).periodIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(1600);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(1600);
// auto discontinuities through ads has correct content position that is not the seek position.
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(2500);
assertThat(newPositions.get(4).contentPositionMs).isEqualTo(2500);
// Content resumes at expected position that is not the seek position.
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).positionMs).isEqualTo(2500);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(2500);
}
@Test
public void seekTo_toSAIMidRolls_playsMidRolls() throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 0,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(2500);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(6))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
// seek adjustment discontinuity
assertThat(oldPositions.get(1).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).periodIndex).isEqualTo(1);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
// auto transition to last frame of first ad period
assertThat(oldPositions.get(2).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(2).periodIndex).isEqualTo(1);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
// auto transition to second ad period
assertThat(oldPositions.get(3).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(3).periodIndex).isEqualTo(2);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(0);
// auto transition to last frame of second ad period
assertThat(oldPositions.get(4).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(4).periodIndex).isEqualTo(2);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
// auto transition to the final content period
assertThat(oldPositions.get(5).periodIndex).isEqualTo(2);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).periodIndex).isEqualTo(3);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).positionMs).isEqualTo(2500);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(2500);
}
@Test
public void seekTo_toPlayedSAIMidRolls_requestedContentPositionNotPropagatedIntoAds()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ 2,
/* isAdPeriodFlags...= */ false,
true,
true,
false);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.pause();
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(2500);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
verify(listener, times(1))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(1).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// seek discontinuity
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
// TODO(bachinger): Incorrect masking. Skipped played prerolls not taken into account by masking
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
}
@Test
public void play_playedSSAIPreMidPostRollsMultiPeriodWindow_contentPeriodTransitionsOnly()
throws Exception {
ArgumentCaptor<PositionInfo> oldPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
ArgumentCaptor<Integer> reasonArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
FakeTimeline adTimeline =
FakeTimeline.createMultiPeriodAdTimeline(
"windowId",
/* numberOfPlayedAds= */ Integer.MAX_VALUE,
/* isAdPeriodFlags...= */ true,
false,
true,
true,
false,
true,
true,
true);
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<Integer> playbackStateCaptor = ArgumentCaptor.forClass(Integer.class);
verify(listener, times(3)).onPlaybackStateChanged(playbackStateCaptor.capture());
assertThat(playbackStateCaptor.getAllValues()).containsExactly(2, 3, 4).inOrder();
verify(listener, times(3))
.onPositionDiscontinuity(
oldPositionArgumentCaptor.capture(),
newPositionArgumentCaptor.capture(),
reasonArgumentCaptor.capture());
assertThat(reasonArgumentCaptor.getAllValues()).containsExactly(0, 0, 0).inOrder();
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
// Auto discontinuity from the empty pre-roll period to the first content period.
assertThat(oldPositions.get(0).periodIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).periodIndex).isEqualTo(1);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
// Auto discontinuity from the first content to the second content period.
assertThat(oldPositions.get(1).periodIndex).isEqualTo(1);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).periodIndex).isEqualTo(4);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1250);
// Auto discontinuity from the second content period to the last frame of the last ad period.
assertThat(oldPositions.get(2).periodIndex).isEqualTo(4);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).periodIndex).isEqualTo(7);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).positionMs).isEqualTo(2500);
}
@Test
public void play_playedSSAIPreMidPostRollsSinglePeriodWindow_noDiscontinuities()
throws Exception {
AdPlaybackState adPlaybackState =
addAdGroupToAdPlaybackState(
new AdPlaybackState("adsId"),
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (3 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (5 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US
+ (9 * C.MICROS_PER_SECOND),
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ C.MICROS_PER_SECOND);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup+ */ 0);
adPlaybackState =
adPlaybackState.withPlayedAd(/* adGroupIndex= */ 3, /* adIndexInAdGroup+ */ 0);
FakeTimeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
"windowId",
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ DEFAULT_WINDOW_DURATION_US,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
/* adPlaybackStates= */ ImmutableList.of(adPlaybackState),
MediaItem.EMPTY));
Listener listener = mock(Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(listener);
AtomicReference<ServerSideAdInsertionMediaSource> sourceReference = new AtomicReference<>();
sourceReference.set(
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(adTimeline, ExoPlayerTestRunner.AUDIO_FORMAT),
contentTimeline -> {
sourceReference
.get()
.setAdPlaybackStates(adTimeline.getAdPlaybackStates(/* windowIndex= */ 0));
return true;
}));
player.setMediaSource(sourceReference.get());
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
long finalPositionMs = player.getCurrentPosition();
player.release();
assertThat(finalPositionMs).isEqualTo(6000);
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
ArgumentCaptor<Integer> playbackStateCaptor = ArgumentCaptor.forClass(Integer.class);
verify(listener, times(3)).onPlaybackStateChanged(playbackStateCaptor.capture());
assertThat(playbackStateCaptor.getAllValues()).containsExactly(2, 3, 4).inOrder();
}
@Test
public void becomingNoisyIgnoredIfBecomingNoisyHandlingIsDisabled() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.play();
player.setHandleAudioBecomingNoisy(false);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
runUntilPendingCommandsAreFullyHandled(player);
boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
player.release();
assertThat(playWhenReadyAfterBroadcast).isTrue();
}
@Test
public void pausesWhenBecomingNoisyIfBecomingNoisyHandlingIsEnabled() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.play();
player.setHandleAudioBecomingNoisy(true);
deliverBroadcast(new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
runUntilPendingCommandsAreFullyHandled(player);
boolean playWhenReadyAfterBroadcast = player.getPlayWhenReady();
player.release();
assertThat(playWhenReadyAfterBroadcast).isFalse();
}
@Test
public void loadControlNeverWantsToLoad_throwsIllegalStateException() {
LoadControl neverLoadingLoadControl =
new DefaultLoadControl() {
@Override
public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
return false;
}
@Override
public boolean shouldStartPlayback(
long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return true;
}
};
// Use chunked data to ensure the player actually needs to continue loading and playing.
FakeAdaptiveDataSet.Factory dataSetFactory =
new FakeAdaptiveDataSet.Factory(
/* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0, new Random(0));
MediaSource chunkedMediaSource =
new FakeAdaptiveMediaSource(
new FakeTimeline(),
new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT)),
new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory()));
ExoPlaybackException exception =
assertThrows(
ExoPlaybackException.class,
() ->
new ExoPlayerTestRunner.Builder(context)
.setLoadControl(neverLoadingLoadControl)
.setMediaSources(chunkedMediaSource)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS));
assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED);
assertThat(exception.getUnexpectedException()).isInstanceOf(IllegalStateException.class);
}
@Test
public void
nextLoadPositionExceedingLoadControlMaxBuffer_whileCurrentLoadInProgress_doesNotThrowException()
throws Exception {
long maxBufferUs = 2 * C.MICROS_PER_SECOND;
LoadControl loadControlWithMaxBufferUs =
new DefaultLoadControl() {
@Override
public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
return bufferedDurationUs < maxBufferUs;
}
@Override
public boolean shouldStartPlayback(
long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return true;
}
};
MediaSource mediaSourceWithLoadInProgress =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
mediaSourceEventDispatcher) {
@Override
public long getBufferedPositionUs() {
// Pretend not to have buffered data yet.
return 0;
}
@Override
public long getNextLoadPositionUs() {
// Set next load position beyond the maxBufferUs configured in the LoadControl.
return Long.MAX_VALUE;
}
@Override
public boolean isLoading() {
return true;
}
};
}
};
FakeRenderer rendererWaitingForData =
new FakeRenderer(C.TRACK_TYPE_VIDEO) {
@Override
public boolean isReady() {
return false;
}
};
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(rendererWaitingForData)
.setLoadControl(loadControlWithMaxBufferUs)
.build();
player.setMediaSource(mediaSourceWithLoadInProgress);
player.prepare();
// Wait until the MediaSource is prepared, i.e. returned its timeline, and at least one
// iteration of doSomeWork after this was run.
TestPlayerRunHelper.runUntilTimelineChanged(player);
runUntilPendingCommandsAreFullyHandled(player);
assertThat(player.getPlayerError()).isNull();
}
@Test
public void loadControlNeverWantsToPlay_playbackDoesNotGetStuck() throws Exception {
LoadControl neverLoadingOrPlayingLoadControl =
new DefaultLoadControl() {
@Override
public boolean shouldContinueLoading(
long playbackPositionUs, long bufferedDurationUs, float playbackSpeed) {
return true;
}
@Override
public boolean shouldStartPlayback(
long bufferedDurationUs,
float playbackSpeed,
boolean rebuffering,
long targetLiveOffsetUs) {
return false;
}
};
// Use chunked data to ensure the player actually needs to continue loading and playing.
FakeAdaptiveDataSet.Factory dataSetFactory =
new FakeAdaptiveDataSet.Factory(
/* chunkDurationUs= */ 500_000, /* bitratePercentStdDev= */ 10.0, new Random(0));
MediaSource chunkedMediaSource =
new FakeAdaptiveMediaSource(
new FakeTimeline(),
new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT)),
new FakeChunkSource.Factory(dataSetFactory, new FakeDataSource.Factory()));
new ExoPlayerTestRunner.Builder(context)
.setLoadControl(neverLoadingOrPlayingLoadControl)
.setMediaSources(chunkedMediaSource)
.build()
.start()
// This throws if playback doesn't finish within timeout.
.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void shortAdFollowedByUnpreparedAd_playbackDoesNotGetStuck() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 2, /* adGroupTimesUs...= */ 0);
long shortAdDurationMs = 1_000;
adPlaybackState =
adPlaybackState.withAdDurationsUs(new long[][] {{shortAdDurationMs, shortAdDurationMs}});
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000),
adPlaybackState));
// Simulate the second ad not being prepared.
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
FakeMediaPeriod.TrackDataFactory.singleSampleWithTimeUs(0),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ id.adIndexInAdGroup == 1);
}
};
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaSource(mediaSource);
player.prepare();
player.play();
// The player is not stuck in the buffering state.
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
}
@Test
public void moveMediaItem() throws Exception {
TimelineWindowDefinition firstWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
TimelineWindowDefinition secondWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
Timeline timeline1 = new FakeTimeline(firstWindowDefinition);
Timeline timeline2 = new FakeTimeline(secondWindowDefinition);
MediaSource mediaSource1 = new FakeMediaSource(timeline1);
MediaSource mediaSource2 = new FakeMediaSource(timeline2);
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 2));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(
/* expectedTimeline= */ null, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
Timeline expectedRealTimeline = new FakeTimeline(firstWindowDefinition, secondWindowDefinition);
Timeline expectedRealTimelineAfterMove =
new FakeTimeline(secondWindowDefinition, firstWindowDefinition);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
exoPlayerTestRunner.assertTimelinesSame(
expectedPlaceholderTimeline, expectedRealTimeline, expectedRealTimelineAfterMove);
}
@Test
public void removeMediaItem() throws Exception {
TimelineWindowDefinition firstWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
TimelineWindowDefinition secondWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
TimelineWindowDefinition thirdWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 3,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
Timeline timeline1 = new FakeTimeline(firstWindowDefinition);
Timeline timeline2 = new FakeTimeline(secondWindowDefinition);
Timeline timeline3 = new FakeTimeline(thirdWindowDefinition);
MediaSource mediaSource1 = new FakeMediaSource(timeline1);
MediaSource mediaSource2 = new FakeMediaSource(timeline2);
MediaSource mediaSource3 = new FakeMediaSource(timeline3);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.removeMediaItem(/* index= */ 0)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2, mediaSource3)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 2),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 3));
Timeline expectedRealTimeline =
new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition);
Timeline expectedRealTimelineAfterRemove =
new FakeTimeline(secondWindowDefinition, thirdWindowDefinition);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
exoPlayerTestRunner.assertTimelinesSame(
expectedPlaceholderTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove);
}
@Test
public void removeMediaItems() throws Exception {
TimelineWindowDefinition firstWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
TimelineWindowDefinition secondWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
TimelineWindowDefinition thirdWindowDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 3,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10000));
Timeline timeline1 = new FakeTimeline(firstWindowDefinition);
Timeline timeline2 = new FakeTimeline(secondWindowDefinition);
Timeline timeline3 = new FakeTimeline(thirdWindowDefinition);
MediaSource mediaSource1 = new FakeMediaSource(timeline1);
MediaSource mediaSource2 = new FakeMediaSource(timeline2);
MediaSource mediaSource3 = new FakeMediaSource(timeline3);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2, mediaSource3)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
Timeline expectedPlaceholderTimeline =
new FakeTimeline(
TimelineWindowDefinition.createPlaceholder(/* tag= */ 1),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 2),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 3));
Timeline expectedRealTimeline =
new FakeTimeline(firstWindowDefinition, secondWindowDefinition, thirdWindowDefinition);
Timeline expectedRealTimelineAfterRemove = new FakeTimeline(firstWindowDefinition);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
exoPlayerTestRunner.assertTimelinesSame(
expectedPlaceholderTimeline, expectedRealTimeline, expectedRealTimelineAfterRemove);
}
@Test
public void clearMediaItems() throws Exception {
Timeline timeline = new FakeTimeline();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.waitForPlaybackState(Player.STATE_READY)
.clearMediaItems()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelinesSame(placeholderTimeline, timeline, Timeline.EMPTY);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */);
}
@Test
public void multipleModificationWithRecursiveListenerInvocations() throws Exception {
Timeline timeline = new FakeTimeline();
MediaSource mediaSource = new FakeMediaSource(timeline);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.clearMediaItems()
.setMediaSources(secondMediaSource)
.waitForTimelineChanged()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertTimelinesSame(
placeholderTimeline,
timeline,
Timeline.EMPTY,
new FakeMediaSource.InitialTimeline(secondTimeline),
secondTimeline);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void modifyPlaylistUnprepared_remainsInIdle_needsPrepareForBuffering() throws Exception {
int[] playbackStates = new int[4];
int[] timelineWindowCounts = new int[4];
int[] maskingPlaybackState = {C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(
placeholderTimeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
.executeRunnable(
new PlaybackStateCollector(/* index= */ 0, playbackStates, timelineWindowCounts))
.clearMediaItems()
.executeRunnable(
new PlaybackStateCollector(/* index= */ 1, playbackStates, timelineWindowCounts))
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.setMediaSource(new FakeMediaSource(), /* startPositionMs= */ 1000);
maskingPlaybackState[0] = player.getPlaybackState();
}
})
.executeRunnable(
new PlaybackStateCollector(/* index= */ 2, playbackStates, timelineWindowCounts))
.addMediaSources(new FakeMediaSource())
.executeRunnable(
new PlaybackStateCollector(/* index= */ 3, playbackStates, timelineWindowCounts))
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 2000)
.prepare()
// The first expected buffering state arrives after prepare but not before.
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(
new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE},
playbackStates);
assertArrayEquals(new int[] {1, 0, 1, 2}, timelineWindowCounts);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING /* first buffering state after prepare */,
Player.STATE_READY,
Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* initial setMediaSources */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* set media items */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* add media items */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source update after prepare */);
Timeline expectedSecondPlaceholderTimeline =
new FakeTimeline(
TimelineWindowDefinition.createPlaceholder(/* tag= */ 0),
TimelineWindowDefinition.createPlaceholder(/* tag= */ 0));
Timeline expectedSecondRealTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000),
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10_000_000));
exoPlayerTestRunner.assertTimelinesSame(
placeholderTimeline,
Timeline.EMPTY,
placeholderTimeline,
expectedSecondPlaceholderTimeline,
expectedSecondRealTimeline);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackState);
}
@Test
public void modifyPlaylistPrepared_remainsInEnded_needsSeekForBuffering() throws Exception {
Timeline timeline = new FakeTimeline();
FakeMediaSource secondMediaSource = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.clearMediaItems()
.waitForPlaybackState(Player.STATE_ENDED)
.addMediaSources(secondMediaSource) // add must not transition to buffering
.waitForTimelineChanged()
.clearMediaItems() // clear must remain in ended
.addMediaSources(secondMediaSource) // add again to be able to test the seek
.waitForTimelineChanged()
.seek(/* positionMs= */ 2_000) // seek must transition to buffering
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, // first buffering
Player.STATE_READY,
Player.STATE_ENDED, // clear playlist
Player.STATE_BUFFERING, // second buffering after seek
Player.STATE_READY,
Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelinesSame(
placeholderTimeline,
timeline,
Timeline.EMPTY,
placeholderTimeline,
timeline,
Timeline.EMPTY,
placeholderTimeline,
timeline);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* playlist cleared */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media items added */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */);
}
@Test
public void stopWithNoReset_modifyingPlaylistRemainsInIdleState_needsPrepareForBuffering()
throws Exception {
Timeline timeline = new FakeTimeline();
FakeMediaSource secondMediaSource = new FakeMediaSource(timeline);
int[] playbackStateHolder = new int[3];
int[] windowCountHolder = new int[3];
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ false)
.executeRunnable(
new PlaybackStateCollector(/* index= */ 0, playbackStateHolder, windowCountHolder))
.clearMediaItems()
.executeRunnable(
new PlaybackStateCollector(/* index= */ 1, playbackStateHolder, windowCountHolder))
.addMediaSources(secondMediaSource)
.executeRunnable(
new PlaybackStateCollector(/* index= */ 2, playbackStateHolder, windowCountHolder))
.prepare()
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(
new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE}, playbackStateHolder);
assertArrayEquals(new int[] {1, 0, 1}, windowCountHolder);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, // first buffering
Player.STATE_READY,
Player.STATE_IDLE, // stop
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelinesSame(
placeholderTimeline, timeline, Timeline.EMPTY, placeholderTimeline, timeline);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, /* source prepared */
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* clear media items */,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item add (masked timeline) */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */);
}
@Test
public void prepareWithInvalidInitialSeek_expectEndedImmediately() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.initialSeek(/* mediaItemIndex= */ 1, C.TIME_UNSET)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelinesSame();
exoPlayerTestRunner.assertTimelineChangeReasonsEqual();
assertArrayEquals(new int[] {1}, currentMediaItemIndices);
}
@Test
public void prepareWhenAlreadyPreparedIsANoop() throws Exception {
Timeline timeline = new FakeTimeline();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_READY).prepare().build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
exoPlayerTestRunner.assertTimelinesSame(placeholderTimeline, timeline);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED /* media item set (masked timeline) */,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE /* source prepared */);
}
@Test
public void seekToIndexLargerThanNumberOfPlaylistItems() throws Exception {
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000_000));
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(
/* isAtomic= */ false,
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT));
int[] currentMediaItemIndices = new int[1];
long[] currentPlaybackPositions = new long[1];
int seekToMediaItemIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[0] = player.getCurrentPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.initialSeek(seekToMediaItemIndex, 5000)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new long[] {5_000}, currentPlaybackPositions);
assertArrayEquals(new int[] {seekToMediaItemIndex}, currentMediaItemIndices);
}
@Test
public void seekToIndexWithEmptyMultiWindowMediaSource() throws Exception {
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000_000));
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ false);
int[] currentMediaItemIndices = new int[2];
long[] currentPlaybackPositions = new long[2];
long[] windowCounts = new long[2];
int seekToMediaItemIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[0] = player.getCurrentPosition();
windowCounts[0] = player.getCurrentTimeline().getWindowCount();
}
})
.executeRunnable(
() ->
concatenatingMediaSource.addMediaSources(
Arrays.asList(
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT))))
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[1] = player.getCurrentPosition();
windowCounts[1] = player.getCurrentTimeline().getWindowCount();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.initialSeek(seekToMediaItemIndex, 5000)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new long[] {0, 2}, windowCounts);
assertArrayEquals(
new int[] {seekToMediaItemIndex, seekToMediaItemIndex}, currentMediaItemIndices);
assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions);
}
@Test
public void emptyMultiWindowMediaSource_doesNotEnterBufferState() throws Exception {
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ false);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG).waitForPlaybackState(Player.STATE_ENDED).build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED);
}
@Test
public void seekToIndexWithEmptyMultiWindowMediaSource_usesLazyPreparation() throws Exception {
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true, /* isDynamic= */ false, /* durationUs= */ 10_000_000));
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ false);
int[] currentMediaItemIndices = new int[2];
long[] currentPlaybackPositions = new long[2];
long[] windowCounts = new long[2];
int seekToMediaItemIndex = 1;
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[0] = player.getCurrentPosition();
windowCounts[0] = player.getCurrentTimeline().getWindowCount();
}
})
.executeRunnable(
() ->
concatenatingMediaSource.addMediaSources(
Arrays.asList(
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, ExoPlayerTestRunner.VIDEO_FORMAT))))
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPlaybackPositions[1] = player.getCurrentPosition();
windowCounts[1] = player.getCurrentTimeline().getWindowCount();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(concatenatingMediaSource)
.setUseLazyPreparation(/* useLazyPreparation= */ true)
.initialSeek(seekToMediaItemIndex, 5000)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new long[] {0, 2}, windowCounts);
assertArrayEquals(
new int[] {seekToMediaItemIndex, seekToMediaItemIndex}, currentMediaItemIndices);
assertArrayEquals(new long[] {5_000, 5_000}, currentPlaybackPositions);
}
@Test
public void
timelineUpdateInMultiWindowMediaSource_removingPeriod_withUnpreparedMaskingMediaPeriod_doesNotThrow()
throws Exception {
TimelineWindowDefinition window1 =
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1);
TimelineWindowDefinition window2 =
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2);
FakeMediaSource mediaSource = new FakeMediaSource(/* timeline= */ null);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
// Wait so that the player can create its unprepared MaskingMediaPeriod.
.waitForPendingPlayerCommands()
// Let the player assign the unprepared period to window1.
.executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window1, window2)))
.waitForTimelineChanged()
// Remove window1 and assume the update is handled without throwing.
.executeRunnable(() -> mediaSource.setNewSourceInfo(new FakeTimeline(window2)))
.waitForTimelineChanged()
.stop()
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Assertion is to not throw while running the action schedule above.
}
@Test
public void setPlayWhenReady_keepsCurrentPosition() throws Exception {
AtomicLong positionAfterSetPlayWhenReady = new AtomicLong(C.TIME_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.playUntilPosition(0, 5000)
.play()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionAfterSetPlayWhenReady.set(player.getCurrentPosition());
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionAfterSetPlayWhenReady.get()).isAtLeast(5000);
}
@Test
public void setPlayWhenReady_correctPositionMasking() throws Exception {
long[] currentPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.playUntilPosition(0, 5000)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentPositionMs[0] = player.getCurrentPosition();
bufferedPositionMs[0] = player.getBufferedPosition();
player.setPlayWhenReady(true);
currentPositionMs[1] = player.getCurrentPosition();
bufferedPositionMs[1] = player.getBufferedPosition();
player.setPlayWhenReady(false);
currentPositionMs[2] = player.getCurrentPosition();
bufferedPositionMs[2] = player.getBufferedPosition();
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(currentPositionMs[0]).isAtLeast(5000);
assertThat(currentPositionMs[1]).isEqualTo(currentPositionMs[0]);
assertThat(currentPositionMs[2]).isEqualTo(currentPositionMs[0]);
assertThat(bufferedPositionMs[0]).isGreaterThan(currentPositionMs[0]);
assertThat(bufferedPositionMs[1]).isEqualTo(bufferedPositionMs[0]);
assertThat(bufferedPositionMs[2]).isEqualTo(bufferedPositionMs[0]);
}
@Test
public void setShuffleMode_correctPositionMasking() throws Exception {
long[] currentPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
long[] bufferedPositionMs = new long[] {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.playUntilPosition(0, 5000)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentPositionMs[0] = player.getCurrentPosition();
bufferedPositionMs[0] = player.getBufferedPosition();
player.setShuffleModeEnabled(true);
currentPositionMs[1] = player.getCurrentPosition();
bufferedPositionMs[1] = player.getBufferedPosition();
player.setShuffleModeEnabled(false);
currentPositionMs[2] = player.getCurrentPosition();
bufferedPositionMs[2] = player.getBufferedPosition();
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(currentPositionMs[0]).isAtLeast(5000);
assertThat(currentPositionMs[1]).isEqualTo(currentPositionMs[0]);
assertThat(currentPositionMs[2]).isEqualTo(currentPositionMs[0]);
assertThat(bufferedPositionMs[0]).isGreaterThan(currentPositionMs[0]);
assertThat(bufferedPositionMs[1]).isEqualTo(bufferedPositionMs[0]);
assertThat(bufferedPositionMs[2]).isEqualTo(bufferedPositionMs[0]);
}
@Test
public void setShuffleOrder_keepsCurrentPosition() throws Exception {
AtomicLong positionAfterSetShuffleOrder = new AtomicLong(C.TIME_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.playUntilPosition(0, 5000)
.setShuffleOrder(new FakeShuffleOrder(/* length= */ 1))
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
positionAfterSetShuffleOrder.set(player.getCurrentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000);
}
@Test
public void setMediaSources_empty_whenEmpty_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
List<MediaSource> listOfTwo =
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource());
player.addMediaSources(/* index= */ 0, listOfTwo);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {0, 0, 0}, currentMediaItemIndices);
}
@Test
public void setMediaItems_resetPosition_resetsPosition() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] currentPositions = {C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1000);
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPositions[0] = player.getCurrentPosition();
List<MediaItem> listOfTwo =
ImmutableList.of(
MediaItem.fromUri(Uri.EMPTY), MediaItem.fromUri(Uri.EMPTY));
player.setMediaItems(listOfTwo, /* resetPosition= */ true);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPositions[1] = player.getCurrentPosition();
}
})
.prepare()
.waitForTimelineChanged()
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0}, currentMediaItemIndices);
assertArrayEquals(new long[] {1000, 0}, currentPositions);
}
@Test
public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingMediaItemIndex()
throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
List<MediaSource> listOfTwo =
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource());
player.addMediaSources(/* index= */ 0, listOfTwo);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, C.TIME_UNSET)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 1, 1}, currentMediaItemIndices);
}
@Test
public void setMediaSources_empty_whenEmpty_invalidInitialSeek_correctMaskingMediaItemIndex()
throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
List<MediaSource> listOfTwo =
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource());
player.addMediaSources(/* index= */ 0, listOfTwo);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 4, C.TIME_UNSET)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {4, 0, 0}, currentMediaItemIndices);
}
@Test
public void setMediaSources_whenEmpty_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {
C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Increase current media item index.
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Current media item index is unchanged.
player.addMediaSource(/* index= */ 2, new FakeMediaSource());
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
MediaSource mediaSource = new FakeMediaSource();
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(mediaSource, mediaSource, mediaSource);
// Increase current media item with multi media item source.
player.addMediaSource(/* index= */ 0, concatenatingMediaSource);
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource();
// Current media item index is unchanged when adding empty source.
player.addMediaSource(/* index= */ 0, concatenatingMediaSource);
currentMediaItemIndices[3] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 1, 4, 4}, currentMediaItemIndices);
}
@Test
public void setMediaSources_whenEmpty_validInitialSeek_correctMasking() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource firstMediaSource = new FakeMediaSource(firstTimeline);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, new Object());
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPositions[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
// Increase current media item index.
player.addMediaSource(/* index= */ 0, secondMediaSource);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPositions[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
currentPositions[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, 2000)
.setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 2, 2}, currentMediaItemIndices);
assertArrayEquals(new long[] {2000, 2000, 2000}, currentPositions);
assertArrayEquals(new long[] {2000, 2000, 2000}, bufferedPositions);
}
@Test
public void setMediaSources_whenEmpty_invalidInitialSeek_correctMasking() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPositions[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
// Increase current media item index.
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPositions[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
currentPositions[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, 2000)
.setMediaSources(new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {0, 1, 1}, currentMediaItemIndices);
assertArrayEquals(new long[] {0, 0, 0}, currentPositions);
assertArrayEquals(new long[] {0, 0, 0}, bufferedPositions);
}
@Test
public void setMediaSources_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
// Increase current media item index.
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {0, 1, 1}, currentMediaItemIndices);
}
@Test
public void setMediaSources_whenIdle_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[4];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with no seek.
player.setMediaSource(new ConcatenatingMediaSource());
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an implicit seek to the current position.
player.setMediaSource(new FakeMediaSource());
maskingPlaybackStates[1] = player.getPlaybackState();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an explicit seek.
player.setMediaSource(
new FakeMediaSource(), /* startPositionMs= */ C.TIME_UNSET);
maskingPlaybackStates[2] = player.getPlaybackState();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with an explicit seek.
player.setMediaSource(
new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET);
maskingPlaybackStates[3] = player.getPlaybackState();
}
})
.prepare()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED);
assertArrayEquals(
new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_IDLE},
maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}
@Test
public void setMediaSources_whenIdle_invalidSeek_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set a media item with an implicit seek to the current position which is
// invalid in the new timeline.
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L)));
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenIdle_noSeek_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with no seek.
player.setMediaSource(new FakeMediaSource());
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenIdle_noSeekEmpty_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set an empty media item with no seek.
player.setMediaSource(new ConcatenatingMediaSource());
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.setMediaSources(new FakeMediaSource())
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_IDLE}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenEnded_correctMaskingPlaybackState() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L);
MediaSource firstMediaSource = new FakeMediaSource(firstTimeline);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L);
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
final int[] maskingPlaybackStates = new int[4];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with an implicit seek to the current position.
player.setMediaSource(new ConcatenatingMediaSource());
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with an explicit seek.
player.setMediaSource(
new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET);
maskingPlaybackStates[1] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an implicit seek to the current position.
player.setMediaSource(firstMediaSource);
maskingPlaybackStates[2] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_READY)
.clearMediaItems()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an explicit seek.
player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET);
maskingPlaybackStates[3] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_ENDED,
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_ENDED,
Player.STATE_BUFFERING,
Player.STATE_READY,
Player.STATE_ENDED);
assertArrayEquals(
new int[] {
Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING
},
maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenEnded_invalidSeek_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an invalid implicit seek to the current position.
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1, 1L)),
/* resetPosition= */ false);
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenEnded_noSeek_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.clearMediaItems()
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with no seek (keep current position).
player.setMediaSource(new FakeMediaSource(), /* resetPosition= */ false);
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void setMediaSources_whenEnded_noSeekEmpty_correctMaskingPlaybackState() throws Exception {
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.clearMediaItems()
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set an empty media item with no seek.
player.setMediaSource(new ConcatenatingMediaSource());
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}
@Test
public void setMediaSources_whenPrepared_correctMaskingPlaybackState() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L);
MediaSource firstMediaSource = new FakeMediaSource(firstTimeline);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L);
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
final int[] maskingPlaybackStates = new int[4];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with an implicit seek to current position.
player.setMediaSource(
new ConcatenatingMediaSource(), /* resetPosition= */ false);
// Expect masking state is ended,
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.setMediaSources(
/* mediaItemIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set empty media item with an explicit seek.
player.setMediaSource(
new ConcatenatingMediaSource(), /* startPositionMs= */ C.TIME_UNSET);
// Expect masking state is ended,
maskingPlaybackStates[1] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.setMediaSources(
/* mediaItemIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an explicit seek.
player.setMediaSource(secondMediaSource, /* startPositionMs= */ C.TIME_UNSET);
// Expect masking state is buffering,
maskingPlaybackStates[2] = player.getPlaybackState();
}
})
.waitForPlaybackState(Player.STATE_READY)
.setMediaSources(
/* mediaItemIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Set media item with an implicit seek to the current position.
player.setMediaSource(secondMediaSource, /* resetPosition= */ false);
// Expect masking state is buffering,
maskingPlaybackStates[3] = player.getPlaybackState();
}
})
.play()
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 3)
.setMediaSources(firstMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready after initial prepare.
Player.STATE_ENDED, // Ended after setting empty source without seek.
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready again after re-setting source.
Player.STATE_ENDED, // Ended after setting empty source with seek.
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready again after re-setting source.
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready after setting media item with seek.
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready again after re-setting source.
Player.STATE_BUFFERING, // Play.
Player.STATE_READY, // Ready after setting media item without seek.
Player.STATE_ENDED);
assertArrayEquals(
new int[] {
Player.STATE_ENDED, Player.STATE_ENDED, Player.STATE_BUFFERING, Player.STATE_BUFFERING
},
maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Initial source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, // Empty source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Set source with seek.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE, // Reset source.
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); // Set source without seek.
}
@Test
public void setMediaSources_whenPrepared_invalidSeek_correctMaskingPlaybackState()
throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L);
MediaSource firstMediaSource = new FakeMediaSource(firstTimeline);
final int[] maskingPlaybackStates = new int[1];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// An implicit, invalid seek picking up the position set by the initial seek.
player.setMediaSource(firstMediaSource, /* resetPosition= */ false);
// Expect masking state is ended,
maskingPlaybackStates[0] = player.getPlaybackState();
}
})
.waitForTimelineChanged()
.setMediaSources(
/* mediaItemIndex= */ 0, /* positionMs= */ C.TIME_UNSET, firstMediaSource)
.waitForPlaybackState(Player.STATE_READY)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setExpectedPlayerEndedCount(/* expectedPlayerEndedCount= */ 2)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new ConcatenatingMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_ENDED, // Empty source has been prepared.
Player.STATE_BUFFERING, // After setting another source.
Player.STATE_READY,
Player.STATE_ENDED);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackStates);
exoPlayerTestRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
}
@Test
public void addMediaSources_whenEmptyInitialSeek_correctPeriodMasking() throws Exception {
final long[] positions = new long[2];
Arrays.fill(positions, C.TIME_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
positions[0] = player.getCurrentPosition();
positions[1] = player.getBufferedPosition();
}
})
.prepare()
.build();
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.initialSeek(/* mediaItemIndex= */ 0, /* positionMs= */ 2000)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new long[] {2000, 2000}, positions);
}
@Test
public void addMediaSources_skipSettingMediaItems_validInitialSeek_correctMasking()
throws Exception {
final int[] currentMediaItemIndices = new int[5];
Arrays.fill(currentMediaItemIndices, C.INDEX_UNSET);
final long[] currentPositions = new long[3];
Arrays.fill(currentPositions, C.TIME_UNSET);
final long[] bufferedPositions = new long[3];
Arrays.fill(bufferedPositions, C.TIME_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
// If the timeline is empty masking variables are used.
currentPositions[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
player.addMediaSource(/* index= */ 0, new ConcatenatingMediaSource());
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
player.addMediaSource(
/* index= */ 0,
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)));
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
currentMediaItemIndices[3] = player.getCurrentMediaItemIndex();
// With a non-empty timeline, we mask the periodId in the playback info.
currentPositions[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[4] = player.getCurrentMediaItemIndex();
// Finally original playbackInfo coming from EPII is used.
currentPositions[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.initialSeek(/* mediaItemIndex= */ 1, 2000)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 1, 1, 2, 2}, currentMediaItemIndices);
assertThat(currentPositions[0]).isEqualTo(2000);
assertThat(currentPositions[1]).isEqualTo(2000);
assertThat(currentPositions[2]).isAtLeast(2000);
assertThat(bufferedPositions[0]).isEqualTo(2000);
assertThat(bufferedPositions[1]).isEqualTo(2000);
assertThat(bufferedPositions[2]).isAtLeast(2000);
}
@Test
public void
testAddMediaSources_skipSettingMediaItems_invalidInitialSeek_correctMaskingMediaItemIndex()
throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
player.addMediaSource(new FakeMediaSource());
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.skipSettingMediaSources()
.initialSeek(/* mediaItemIndex= */ 1, C.TIME_UNSET)
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0, 0}, currentMediaItemIndices);
}
@Test
public void moveMediaItems_correctMaskingMediaItemIndex() throws Exception {
Timeline timeline = new FakeTimeline();
MediaSource firstMediaSource = new FakeMediaSource(timeline);
MediaSource secondMediaSource = new FakeMediaSource(timeline);
MediaSource thirdMediaSource = new FakeMediaSource(timeline);
final int[] currentMediaItemIndices = {
C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move the current item down in the playlist.
player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1);
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move the current item up in the playlist.
player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.seek(/* mediaItemIndex= */ 2, C.TIME_UNSET)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move items from before to behind the current item.
player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1);
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move items from behind to before the current item.
player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 3, /* newIndex= */ 0);
currentMediaItemIndices[3] = player.getCurrentMediaItemIndex();
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move items from before to before the current item.
// No change in currentMediaItemIndex.
player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 1);
currentMediaItemIndices[4] = player.getCurrentMediaItemIndex();
}
})
.seek(/* mediaItemIndex= */ 0, C.TIME_UNSET)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Move items from behind to behind the current item.
// No change in currentMediaItemIndex.
player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 2, /* newIndex= */ 2);
currentMediaItemIndices[5] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0, 0, 2, 2, 0}, currentMediaItemIndices);
}
@Test
public void moveMediaItems_unprepared_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Increase current media item index.
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
player.moveMediaItem(/* currentIndex= */ 0, /* newIndex= */ 1);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.prepare()
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {0, 1, 1}, currentMediaItemIndices);
}
@Test
public void removeMediaItems_correctMaskingMediaItemIndex() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Decrease current media item index.
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
player.removeMediaItem(/* index= */ 0);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0}, currentMediaItemIndices);
}
@Test
public void removeMediaItems_currentItemRemoved_correctMasking() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
final long[] currentPositions = {C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPositions = {C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Remove the current item.
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPositions[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
player.removeMediaItem(/* index= */ 1);
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPositions[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ 5000)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 1}, currentMediaItemIndices);
assertThat(currentPositions[0]).isAtLeast(5000L);
assertThat(bufferedPositions[0]).isAtLeast(5000L);
assertThat(currentPositions[1]).isEqualTo(0);
assertThat(bufferedPositions[1]).isAtLeast(0);
}
@Test
public void removeMediaItems_currentItemRemovedThatIsTheLast_correctMasking() throws Exception {
Timeline firstTimeline = new FakeTimeline(/* windowCount= */ 1, 1L);
MediaSource firstMediaSource = new FakeMediaSource(firstTimeline);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 1, 2L);
MediaSource secondMediaSource = new FakeMediaSource(secondTimeline);
Timeline thirdTimeline = new FakeTimeline(/* windowCount= */ 1, 3L);
MediaSource thirdMediaSource = new FakeMediaSource(thirdTimeline);
Timeline fourthTimeline = new FakeTimeline(/* windowCount= */ 1, 3L);
MediaSource fourthMediaSource = new FakeMediaSource(fourthTimeline);
final int[] currentMediaItemIndices = new int[9];
Arrays.fill(currentMediaItemIndices, C.INDEX_UNSET);
final int[] maskingPlaybackStates = new int[4];
Arrays.fill(maskingPlaybackStates, C.INDEX_UNSET);
final long[] currentPositions = new long[3];
Arrays.fill(currentPositions, C.TIME_UNSET);
final long[] bufferedPositions = new long[3];
Arrays.fill(bufferedPositions, C.TIME_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Expect the current media item index to be 2 after seek.
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPositions[0] = player.getCurrentPosition();
bufferedPositions[0] = player.getBufferedPosition();
player.removeMediaItem(/* index= */ 2);
// Expect the current media item index to be 0
// (default position of timeline after not finding subsequent period).
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
// Transition to ENDED.
maskingPlaybackStates[0] = player.getPlaybackState();
currentPositions[1] = player.getCurrentPosition();
bufferedPositions[1] = player.getBufferedPosition();
}
})
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Expects the current media item index still on 0.
currentMediaItemIndices[2] = player.getCurrentMediaItemIndex();
// Insert an item at begin when the playlist is not empty.
player.addMediaSource(/* index= */ 0, thirdMediaSource);
// Expects the current media item index to be (0 + 1) after insertion at begin.
currentMediaItemIndices[3] = player.getCurrentMediaItemIndex();
// Remains in ENDED.
maskingPlaybackStates[1] = player.getPlaybackState();
currentPositions[2] = player.getCurrentPosition();
bufferedPositions[2] = player.getBufferedPosition();
}
})
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[4] = player.getCurrentMediaItemIndex();
// Implicit seek to the current media item index, which is out of bounds in new
// timeline.
player.setMediaSource(fourthMediaSource, /* resetPosition= */ false);
// 0 after reset.
currentMediaItemIndices[5] = player.getCurrentMediaItemIndex();
// Invalid seek, so we remain in ENDED.
maskingPlaybackStates[2] = player.getPlaybackState();
}
})
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[6] = player.getCurrentMediaItemIndex();
// Explicit seek to (0, C.TIME_UNSET). Player transitions to BUFFERING.
player.setMediaSource(fourthMediaSource, /* startPositionMs= */ 5000);
// 0 after explicit seek.
currentMediaItemIndices[7] = player.getCurrentMediaItemIndex();
// Transitions from ENDED to BUFFERING after explicit seek.
maskingPlaybackStates[3] = player.getPlaybackState();
}
})
.waitForTimelineChanged()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Check whether actual media item index is equal masking index from above.
currentMediaItemIndices[8] = player.getCurrentMediaItemIndex();
}
})
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 2, /* positionMs= */ C.TIME_UNSET)
.setExpectedPlayerEndedCount(2)
.setMediaSources(firstMediaSource, secondMediaSource, thirdMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
// Expect reset of masking to first media item.
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING,
Player.STATE_READY, // Ready after initial prepare.
Player.STATE_ENDED, // ended after removing current media item index
Player.STATE_BUFFERING, // buffers after set items with seek
Player.STATE_READY,
Player.STATE_ENDED);
assertArrayEquals(
new int[] {
Player.STATE_ENDED, // ended after removing current media item index
Player.STATE_ENDED, // adding items does not change state
Player.STATE_ENDED, // set items with seek to current position.
Player.STATE_BUFFERING
}, // buffers after set items with seek
maskingPlaybackStates);
assertArrayEquals(new int[] {2, 0, 0, 1, 1, 0, 0, 0, 0}, currentMediaItemIndices);
assertThat(currentPositions[0]).isEqualTo(0);
assertThat(currentPositions[1]).isEqualTo(0);
assertThat(currentPositions[2]).isEqualTo(0);
assertThat(bufferedPositions[0]).isGreaterThan(0);
assertThat(bufferedPositions[1]).isEqualTo(0);
assertThat(bufferedPositions[2]).isEqualTo(0);
}
@Test
public void removeMediaItems_removeTailWithCurrentWindow_whenIdle_finishesPlayback()
throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.waitForPendingPlayerCommands()
.removeMediaItem(/* index= */ 1)
.prepare()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertPlaybackStatesEqual(
Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_ENDED);
}
@Test
public void clearMediaItems_correctMasking() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
final int[] maskingPlaybackState = {C.INDEX_UNSET};
final long[] currentPosition = {C.TIME_UNSET, C.TIME_UNSET};
final long[] bufferedPosition = {C.TIME_UNSET, C.TIME_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_BUFFERING)
.playUntilPosition(/* mediaItemIndex= */ 1, /* positionMs= */ 150)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentPosition[0] = player.getCurrentPosition();
bufferedPosition[0] = player.getBufferedPosition();
player.clearMediaItems();
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentPosition[1] = player.getCurrentPosition();
bufferedPosition[1] = player.getBufferedPosition();
maskingPlaybackState[0] = player.getPlaybackState();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(new int[] {1, 0}, currentMediaItemIndices);
assertThat(currentPosition[0]).isAtLeast(150);
assertThat(currentPosition[1]).isEqualTo(0);
assertThat(bufferedPosition[0]).isAtLeast(150);
assertThat(bufferedPosition[1]).isEqualTo(0);
assertArrayEquals(new int[] {1, 0}, currentMediaItemIndices);
assertArrayEquals(new int[] {Player.STATE_ENDED}, maskingPlaybackState);
}
@Test
public void clearMediaItems_unprepared_correctMaskingMediaItemIndex_notEnded() throws Exception {
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET};
final int[] currentStates = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
// Wait for initial seek to be fully handled by internal player.
.waitForPositionDiscontinuity()
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
currentMediaItemIndices[0] = player.getCurrentMediaItemIndex();
currentStates[0] = player.getPlaybackState();
player.clearMediaItems();
currentMediaItemIndices[1] = player.getCurrentMediaItemIndex();
currentStates[1] = player.getPlaybackState();
}
})
.prepare()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
// Transitions to ended when prepared with zero media items.
currentStates[2] = player.getPlaybackState();
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.initialSeek(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET)
.setMediaSources(new FakeMediaSource(), new FakeMediaSource())
.setActionSchedule(actionSchedule)
.build()
.start(/* doPrepare= */ false)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertArrayEquals(
new int[] {Player.STATE_IDLE, Player.STATE_IDLE, Player.STATE_ENDED}, currentStates);
assertArrayEquals(new int[] {1, 0}, currentMediaItemIndices);
}
@Test
public void errorThrownDuringPlaylistUpdate_keepsConsistentPlayerState() {
FakeMediaSource source1 =
new FakeMediaSource(
new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
FakeMediaSource source2 =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT);
AtomicInteger audioRendererEnableCount = new AtomicInteger(0);
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer =
new FakeRenderer(C.TRACK_TYPE_AUDIO) {
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
if (audioRendererEnableCount.incrementAndGet() == 2) {
// Fail when enabling the renderer for the second time during the playlist update.
throw createRendererException(
new IllegalStateException(),
ExoPlayerTestRunner.AUDIO_FORMAT,
PlaybackException.ERROR_CODE_UNSPECIFIED);
}
}
};
AtomicReference<Timeline> timelineAfterError = new AtomicReference<>();
AtomicReference<TracksInfo> trackInfosAfterError = new AtomicReference<>();
AtomicReference<TrackSelectionArray> trackSelectionsAfterError = new AtomicReference<>();
AtomicInteger mediaItemIndexAfterError = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addAnalyticsListener(
new AnalyticsListener() {
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
timelineAfterError.set(player.getCurrentTimeline());
trackInfosAfterError.set(player.getCurrentTracksInfo());
trackSelectionsAfterError.set(player.getCurrentTrackSelections());
mediaItemIndexAfterError.set(player.getCurrentMediaItemIndex());
}
});
}
})
.pause()
// Wait until fully buffered so that the new renderer can be enabled immediately.
.waitForIsLoading(true)
.waitForIsLoading(false)
.removeMediaItem(0)
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(source1, source2)
.setActionSchedule(actionSchedule)
.setRenderers(videoRenderer, audioRenderer)
.build();
assertThrows(
ExoPlaybackException.class,
() ->
testRunner
.start(/* doPrepare= */ true)
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS));
assertThat(timelineAfterError.get().getWindowCount()).isEqualTo(1);
assertThat(mediaItemIndexAfterError.get()).isEqualTo(0);
assertThat(trackInfosAfterError.get().getTrackGroupInfos()).hasSize(1);
assertThat(trackInfosAfterError.get().getTrackGroupInfos().get(0).getTrackFormat(0))
.isEqualTo(ExoPlayerTestRunner.AUDIO_FORMAT);
assertThat(trackSelectionsAfterError.get().get(0)).isNull(); // Video renderer.
assertThat(trackSelectionsAfterError.get().get(1)).isNotNull(); // Audio renderer.
}
@Test
public void seekToCurrentPosition_inEndedState_switchesToBufferingStateAndContinuesPlayback()
throws Exception {
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount = */ 1));
AtomicInteger mediaItemIndexAfterFinalEndedState = new AtomicInteger();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_ENDED)
.addMediaSources(mediaSource)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.seekTo(player.getCurrentPosition());
}
})
.waitForPlaybackState(Player.STATE_READY)
.waitForPlaybackState(Player.STATE_ENDED)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
mediaItemIndexAfterFinalEndedState.set(player.getCurrentMediaItemIndex());
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(mediaItemIndexAfterFinalEndedState.get()).isEqualTo(1);
}
@Test
public void pauseAtEndOfMediaItems_pausesPlaybackBeforeTransitioningToTheNextItem()
throws Exception {
TimelineWindowDefinition timelineWindowDefinition =
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND);
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition));
AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET);
AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET);
AtomicInteger mediaItemIndexAfterPause = new AtomicInteger(C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlayWhenReady(true)
.waitForPlayWhenReady(false)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playbackStateAfterPause.set(player.getPlaybackState());
mediaItemIndexAfterPause.set(player.getCurrentMediaItemIndex());
positionAfterPause.set(player.getContentPosition());
}
})
.play()
.build();
new ExoPlayerTestRunner.Builder(context)
.setPauseAtEndOfMediaItems(true)
.setMediaSources(mediaSource, mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_READY);
assertThat(mediaItemIndexAfterPause.get()).isEqualTo(0);
assertThat(positionAfterPause.get()).isEqualTo(10_000);
}
@Test
public void pauseAtEndOfMediaItems_pausesPlaybackWhenEnded() throws Exception {
TimelineWindowDefinition timelineWindowDefinition =
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND);
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(timelineWindowDefinition));
AtomicInteger playbackStateAfterPause = new AtomicInteger(C.INDEX_UNSET);
AtomicLong positionAfterPause = new AtomicLong(C.TIME_UNSET);
AtomicInteger mediaItemIndexAfterPause = new AtomicInteger(C.INDEX_UNSET);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlayWhenReady(true)
.waitForPlayWhenReady(false)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playbackStateAfterPause.set(player.getPlaybackState());
mediaItemIndexAfterPause.set(player.getCurrentMediaItemIndex());
positionAfterPause.set(player.getContentPosition());
}
})
.build();
new ExoPlayerTestRunner.Builder(context)
.setPauseAtEndOfMediaItems(true)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(playbackStateAfterPause.get()).isEqualTo(Player.STATE_ENDED);
assertThat(mediaItemIndexAfterPause.get()).isEqualTo(0);
assertThat(positionAfterPause.get()).isEqualTo(10_000);
}
@Test
public void
infiniteLoading_withSmallAllocations_oomIsPreventedByLoadControl_andThrowsStuckBufferingIllegalStateException() {
DefaultLoadControl loadControl =
new DefaultLoadControl.Builder()
.setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE)
.build();
// Return no end of stream signal to prevent playback from ending.
FakeMediaPeriod.TrackDataFactory trackDataWithoutEos = (format, periodId) -> ImmutableList.of();
MediaSource continuouslyAllocatingMediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
trackDataWithoutEos,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
private final List<Allocation> allocations = new ArrayList<>();
private Callback callback;
@Override
public synchronized void prepare(Callback callback, long positionUs) {
this.callback = callback;
super.prepare(callback, positionUs);
}
@Override
public long getBufferedPositionUs() {
// Pretend not to make loading progress, so that continueLoading keeps being called.
return 0;
}
@Override
public long getNextLoadPositionUs() {
// Pretend not to make loading progress, so that continueLoading keeps being called.
return 0;
}
@Override
public boolean continueLoading(long positionUs) {
allocations.add(allocator.allocate());
callback.onContinueLoadingRequested(this);
return true;
}
};
}
};
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(continuouslyAllocatingMediaSource)
.setLoadControl(loadControl)
.build();
ExoPlaybackException exception =
assertThrows(
ExoPlaybackException.class, () -> testRunner.start().blockUntilEnded(TIMEOUT_MS));
assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_UNEXPECTED);
assertThat(exception.getUnexpectedException()).isInstanceOf(IllegalStateException.class);
}
@Test
public void loading_withLargeAllocationCausingOom_playsRemainingMediaAndThenThrows() {
Loader.Loadable loadable =
new Loader.Loadable() {
@SuppressWarnings("UnusedVariable")
@Override
public void load() throws IOException {
@SuppressWarnings("unused") // This test needs the allocation to cause an OOM.
byte[] largeBuffer = new byte[Integer.MAX_VALUE];
}
@Override
public void cancelLoad() {}
};
// Create 3 samples without end of stream signal to test that all 3 samples are
// still played before the sample stream exception is thrown.
FakeSampleStreamItem sample =
oneByteSample(
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
C.BUFFER_FLAG_KEY_FRAME);
FakeMediaPeriod.TrackDataFactory threeSamplesWithoutEos =
(format, mediaPeriodId) -> ImmutableList.of(sample, sample, sample);
MediaSource largeBufferAllocatingMediaSource =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
threeSamplesWithoutEos,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
private final Loader loader = new Loader("ExoPlayerTest");
@Override
public boolean continueLoading(long positionUs) {
super.continueLoading(positionUs);
if (!loader.isLoading()) {
loader.startLoading(
loadable, new FakeLoaderCallback(), /* defaultMinRetryCount= */ 1);
}
return true;
}
@Override
protected FakeSampleStream createSampleStream(
Allocator allocator,
@Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
Format initialFormat,
List<FakeSampleStreamItem> fakeSampleStreamItems) {
return new FakeSampleStream(
allocator,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
initialFormat,
fakeSampleStreamItems) {
@Override
public void maybeThrowError() throws IOException {
loader.maybeThrowError();
}
};
}
};
}
};
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(largeBufferAllocatingMediaSource)
.setRenderers(renderer)
.build();
ExoPlaybackException exception =
assertThrows(
ExoPlaybackException.class, () -> testRunner.start().blockUntilEnded(TIMEOUT_MS));
assertThat(exception.type).isEqualTo(ExoPlaybackException.TYPE_SOURCE);
assertThat(exception.getSourceException()).isInstanceOf(Loader.UnexpectedLoaderException.class);
assertThat(exception.getSourceException().getCause()).isInstanceOf(OutOfMemoryError.class);
assertThat(renderer.sampleBufferReadCount).isEqualTo(3);
}
@Test
public void seekTo_whileReady_callsOnIsPlayingChanged() throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.seek(/* positionMs= */ 0)
.waitForPlaybackState(Player.STATE_ENDED)
.build();
List<Boolean> onIsPlayingChanges = new ArrayList<>();
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onIsPlayingChanged(boolean isPlaying) {
onIsPlayingChanges.add(isPlaying);
}
};
new ExoPlayerTestRunner.Builder(context)
.setPlayerListener(playerListener)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(onIsPlayingChanges).containsExactly(true, false, true, false).inOrder();
}
@Test
public void multipleListenersAndMultipleCallbacks_callbacksAreOrderedByType() throws Exception {
String playWhenReadyChange1 = "playWhenReadyChange1";
String playWhenReadyChange2 = "playWhenReadyChange2";
String isPlayingChange1 = "isPlayingChange1";
String isPlayingChange2 = "isPlayingChange2";
ArrayList<String> events = new ArrayList<>();
Player.Listener playerListener1 =
new Player.Listener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
events.add(playWhenReadyChange1);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.add(isPlayingChange1);
}
};
Player.Listener playerListener2 =
new Player.Listener() {
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
events.add(playWhenReadyChange2);
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
events.add(isPlayingChange2);
}
};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addListener(playerListener1);
player.addListener(playerListener2);
}
})
.waitForPlaybackState(Player.STATE_READY)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(events)
.containsExactly(
playWhenReadyChange1,
playWhenReadyChange2,
isPlayingChange1,
isPlayingChange2,
isPlayingChange1,
isPlayingChange2)
.inOrder();
}
/**
* This tests that renderer offsets and buffer times in the renderer are set correctly even when
* the sources have a window-to-period offset and a non-zero default start position. The start
* offset of the first source is also updated during preparation to make sure the player adapts
* everything accordingly.
*/
@Test
public void
playlistWithMediaWithStartOffsets_andStartOffsetChangesDuringPreparation_appliesCorrectRenderingOffsetToAllPeriods()
throws Exception {
List<Long> rendererStreamOffsetsUs = new ArrayList<>();
List<Long> firstBufferTimesUsWithOffset = new ArrayList<>();
FakeRenderer renderer =
new FakeRenderer(C.TRACK_TYPE_VIDEO) {
boolean pendingFirstBufferTime = false;
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
rendererStreamOffsetsUs.add(offsetUs);
pendingFirstBufferTime = true;
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
if (pendingFirstBufferTime) {
firstBufferTimesUsWithOffset.add(bufferTimeUs);
pendingFirstBufferTime = false;
}
return super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
}
};
Timeline timelineWithOffsets =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ new Object(),
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US,
/* defaultPositionUs= */ 4_567_890,
/* windowOffsetInFirstPeriodUs= */ 1_234_567,
AdPlaybackState.NONE));
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
long firstSampleTimeUs = 4_567_890 + 1_234_567;
FakeMediaSource firstMediaSource =
new FakeMediaSource(
/* timeline= */ null,
DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, unusedMediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM),
ExoPlayerTestRunner.VIDEO_FORMAT);
FakeMediaSource secondMediaSource =
new FakeMediaSource(
timelineWithOffsets,
DrmSessionManager.DRM_UNSUPPORTED,
(unusedFormat, unusedMediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM),
ExoPlayerTestRunner.VIDEO_FORMAT);
player.setMediaSources(ImmutableList.of(firstMediaSource, secondMediaSource));
// Start playback and wait until player is idly waiting for an update of the first source.
player.prepare();
player.play();
runUntilPendingCommandsAreFullyHandled(player);
// Update media with a non-zero default start position and window offset.
firstMediaSource.setNewSourceInfo(timelineWithOffsets);
// Wait until player transitions to second source (which also has non-zero offsets).
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
player.release();
assertThat(rendererStreamOffsetsUs).hasSize(2);
assertThat(firstBufferTimesUsWithOffset).hasSize(2);
// Assert that the offsets and buffer times match the expected sample time.
assertThat(firstBufferTimesUsWithOffset.get(0))
.isEqualTo(rendererStreamOffsetsUs.get(0) + firstSampleTimeUs);
assertThat(firstBufferTimesUsWithOffset.get(1))
.isEqualTo(rendererStreamOffsetsUs.get(1) + firstSampleTimeUs);
// Assert that the second source continues rendering seamlessly at the point where the first one
// ended.
long periodDurationUs =
timelineWithOffsets.getPeriod(/* periodIndex= */ 0, new Timeline.Period()).durationUs;
assertThat(firstBufferTimesUsWithOffset.get(1))
.isEqualTo(rendererStreamOffsetsUs.get(0) + periodDurationUs);
}
@Test
public void mediaItemOfSources_correctInTimelineWindows() throws Exception {
TimelineWindowDefinition window1 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake1"));
FakeMediaSource fakeMediaSource1 = new FakeMediaSource(new FakeTimeline(window1));
TimelineWindowDefinition window2 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake2"));
FakeMediaSource fakeMediaSource2 = new FakeMediaSource(new FakeTimeline(window2));
TimelineWindowDefinition window3 =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 3,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs = */ 100_000,
/* defaultPositionUs = */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
ImmutableList.of(AdPlaybackState.NONE),
MediaItem.fromUri("http://foo.bar/fake3"));
FakeMediaSource fakeMediaSource3 = new FakeMediaSource(new FakeTimeline(window3));
final Player[] playerHolder = {null};
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
playerHolder[0] = player;
}
})
.waitForPlaybackState(Player.STATE_READY)
.seek(/* positionMs= */ 0)
.play()
.build();
List<MediaItem> currentMediaItems = new ArrayList<>();
List<MediaItem> mediaItemsInTimeline = new ArrayList<>();
Player.Listener playerListener =
new Player.Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
if (reason != Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
return;
}
Window window = new Window();
for (int i = 0; i < timeline.getWindowCount(); i++) {
mediaItemsInTimeline.add(timeline.getWindow(i, window).mediaItem);
}
}
@Override
public void onPositionDiscontinuity(int reason) {
if (reason == Player.DISCONTINUITY_REASON_SEEK
|| reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
currentMediaItems.add(playerHolder[0].getCurrentMediaItem());
}
}
};
new ExoPlayerTestRunner.Builder(context)
.setPlayerListener(playerListener)
.setActionSchedule(actionSchedule)
.setMediaSources(fakeMediaSource1, fakeMediaSource2, fakeMediaSource3)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
assertThat(currentMediaItems.get(0).localConfiguration.uri.toString())
.isEqualTo("http://foo.bar/fake1");
assertThat(currentMediaItems.get(1).localConfiguration.uri.toString())
.isEqualTo("http://foo.bar/fake2");
assertThat(currentMediaItems.get(2).localConfiguration.uri.toString())
.isEqualTo("http://foo.bar/fake3");
assertThat(mediaItemsInTimeline).containsExactlyElementsIn(currentMediaItems);
}
@Test
public void setMediaSource_notifiesMediaItemTransition() {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSource(mediaSource);
assertThat(reportedMediaItems).containsExactly(mediaSource.getMediaItem());
assertThat(reportedTransitionReasons)
.containsExactly(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
player.release();
}
@Test
public void setMediaSource_replaceWithSameMediaItem_notifiesMediaItemTransition()
throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.setMediaSource(mediaSource);
assertThat(reportedMediaItems)
.containsExactly(mediaSource.getMediaItem(), mediaSource.getMediaItem());
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
player.release();
}
@Test
public void automaticWindowTransition_notifiesMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
assertThat(reportedMediaItems)
.containsExactly(mediaSource1.getMediaItem(), mediaSource2.getMediaItem())
.inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)
.inOrder();
player.release();
}
@Test
public void clearMediaItems_notifiesMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
player.play();
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.clearMediaItems();
assertThat(reportedMediaItems)
.containsExactly(mediaSource1.getMediaItem(), mediaSource2.getMediaItem(), null)
.inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
.inOrder();
player.release();
}
@Test
public void seekTo_otherWindow_notifiesMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 2000);
assertThat(reportedMediaItems)
.containsExactly(mediaSource1.getMediaItem(), mediaSource2.getMediaItem())
.inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)
.inOrder();
player.release();
}
@Test
public void seekTo_sameWindow_doesNotNotifyMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 2000);
assertThat(reportedMediaItems).containsExactly(mediaSource1.getMediaItem()).inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
.inOrder();
player.release();
}
@Test
public void repeat_notifiesMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setRepeatMode(Player.REPEAT_MODE_ONE);
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
player.play();
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
runUntilPositionDiscontinuity(player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
runUntilPlaybackState(player, Player.STATE_ENDED);
assertThat(reportedMediaItems)
.containsExactly(
mediaSource1.getMediaItem(),
mediaSource1.getMediaItem(),
mediaSource1.getMediaItem(),
mediaSource2.getMediaItem())
.inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT,
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)
.inOrder();
player.release();
}
// Tests deprecated stop(boolean reset)
@SuppressWarnings("deprecation")
@Test
public void stop_withReset_notifiesMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.stop(/* reset= */ true);
assertThat(reportedMediaItems).containsExactly(mediaSource1.getMediaItem(), null).inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
.inOrder();
player.release();
}
@Test
public void stop_withoutReset_doesNotNotifyMediaItemTransition() throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
MediaSource mediaSource1 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
MediaSource mediaSource2 = FakeMediaSource.createWithWindowId(/* windowId= */ new Object());
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSources(ImmutableList.of(mediaSource1, mediaSource2));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.stop();
assertThat(reportedMediaItems).containsExactly(mediaSource1.getMediaItem()).inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
.inOrder();
player.release();
}
@Test
public void timelineRefresh_withModifiedMediaItem_doesNotNotifyMediaItemTransition()
throws Exception {
List<MediaItem> reportedMediaItems = new ArrayList<>();
List<Integer> reportedTransitionReasons = new ArrayList<>();
List<Timeline> reportedTimelines = new ArrayList<>();
MediaItem initialMediaItem = FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(0).build();
TimelineWindowDefinition initialWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
ImmutableList.of(AdPlaybackState.NONE),
initialMediaItem);
TimelineWindowDefinition secondWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
ImmutableList.of(AdPlaybackState.NONE),
initialMediaItem.buildUpon().setTag(1).build());
FakeTimeline timeline = new FakeTimeline(initialWindow);
FakeTimeline newTimeline = new FakeTimeline(secondWindow);
FakeMediaSource mediaSource = new FakeMediaSource(timeline);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(
new Listener() {
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
reportedTimelines.add(timeline);
}
@Override
public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) {
reportedMediaItems.add(mediaItem);
reportedTransitionReasons.add(reason);
}
});
player.setMediaSource(mediaSource);
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
mediaSource.setNewSourceInfo(newTimeline);
runUntilPlaybackState(player, Player.STATE_ENDED);
assertTimelinesSame(
reportedTimelines, ImmutableList.of(placeholderTimeline, timeline, newTimeline));
assertThat(reportedMediaItems).containsExactly(initialMediaItem).inOrder();
assertThat(reportedTransitionReasons)
.containsExactly(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)
.inOrder();
player.release();
}
@Test
public void isCommandAvailable_isTrueForAvailableCommands() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
assertThat(player.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_PREPARE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_STOP)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SEEK_BACK)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_FORWARD)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TIMELINE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_VOLUME)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_VOLUME)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TEXT)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)).isTrue();
assertThat(player.isCommandAvailable(COMMAND_GET_TRACK_INFOS)).isTrue();
}
@Test
public void isCommandAvailable_duringAd_isFalseForSeekCommands() throws Exception {
AdPlaybackState adPlaybackState =
new AdPlaybackState(/* adsId= */ new Object(), /* adGroupTimesUs...= */ 0)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.EMPTY)
.withAdDurationsUs(/* adDurationUs= */ new long[][] {{Util.msToUs(4_000)}});
Timeline adTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10_000),
adPlaybackState));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(
ImmutableList.of(
new FakeMediaSource(), new FakeMediaSource(adTimeline), new FakeMediaSource()));
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_BACK)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_FORWARD)).isFalse();
}
@Test
public void isCommandAvailable_duringUnseekableItem_isFalseForSeekInCurrentCommands()
throws Exception {
Timeline timelineWithUnseekableWindow =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ false,
/* isDynamic= */ false,
/* durationUs= */ Util.msToUs(10_000)));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(new FakeMediaSource(timelineWithUnseekableWindow));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_BACK)).isFalse();
assertThat(player.isCommandAvailable(COMMAND_SEEK_FORWARD)).isFalse();
}
@Test
public void isCommandAvailable_duringUnseekableLiveItem_isFalseForSeekToPrevious()
throws Exception {
Timeline timelineWithUnseekableLiveWindow =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ false,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ 10_000_000,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(new FakeMediaSource(timelineWithUnseekableLiveWindow));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)).isFalse();
}
@Test
public void
isCommandAvailable_duringUnseekableLiveItemWithPreviousWindow_isTrueForSeekToPrevious()
throws Exception {
Timeline timelineWithUnseekableLiveWindow =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0),
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ false,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ 10_000_000,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(new FakeMediaSource(timelineWithUnseekableLiveWindow));
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)).isTrue();
}
@Test
public void isCommandAvailable_duringLiveItem_isTrueForSeekToNext() throws Exception {
Timeline timelineWithLiveWindow =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ C.TIME_UNSET,
/* defaultPositionUs= */ 10_000_000,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(new FakeMediaSource(timelineWithLiveWindow));
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
assertThat(player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)).isTrue();
}
@Test
public void seekTo_nextWindow_notifiesAvailableCommandsChanged() {
Player.Commands commandsWithSeekToPreviousWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Commands commandsWithSeekToPreviousAndNextWindow =
createWithDefaultCommands(
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void seekTo_previousWindow_notifiesAvailableCommandsChanged() {
Player.Commands commandsWithSeekToPreviousWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Commands commandsWithSeekToPreviousAndNextWindow =
createWithDefaultCommands(
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0);
player.addMediaSources(
ImmutableList.of(
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void seekTo_sameWindow_doesNotNotifyAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(ImmutableList.of(new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 200);
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 100);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void automaticWindowTransition_notifiesAvailableCommandsChanged() throws Exception {
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Commands commandsWithSeekInCurrentAndToNextWindow =
createWithDefaultCommands(
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT,
COMMAND_SEEK_BACK,
COMMAND_SEEK_FORWARD);
Player.Commands commandsWithSeekInCurrentAndToPreviousWindow =
createWithDefaultCommands(
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_BACK,
COMMAND_SEEK_FORWARD);
Player.Commands commandsWithSeekAnywhere =
createWithDefaultCommands(
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT,
COMMAND_SEEK_BACK,
COMMAND_SEEK_FORWARD);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource(),
new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 1);
runUntilPendingCommandsAreFullyHandled(player);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekAnywhere);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
playUntilStartOfMediaItem(player, /* mediaItemIndex= */ 2);
runUntilPendingCommandsAreFullyHandled(player);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekInCurrentAndToPreviousWindow);
verify(mockListener, times(4)).onAvailableCommandsChanged(any());
}
@Test
public void addMediaSource_atTheEnd_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.addMediaSource(new FakeMediaSource());
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void addMediaSource_atTheStart_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToPreviousWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.addMediaSource(/* index= */ 0, new FakeMediaSource());
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void removeMediaItem_atTheEnd_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Commands emptyTimelineCommands = createWithDefaultCommands(/* isTimelineEmpty */ true);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 2);
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 1);
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 0);
verify(mockListener).onAvailableCommandsChanged(emptyTimelineCommands);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void removeMediaItem_atTheStart_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToPreviousWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
Player.Commands emptyTimelineCommands = createWithDefaultCommands(/* isTimelineEmpty */ true);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0);
player.addMediaSources(
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 0);
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 0);
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 0);
verify(mockListener).onAvailableCommandsChanged(emptyTimelineCommands);
verify(mockListener, times(3)).onAvailableCommandsChanged(any());
}
@Test
public void removeMediaItem_current_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.removeMediaItem(/* index= */ 0);
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_all_notifiesAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Commands commandsWithSeekToPreviousAndNextWindow =
createWithDefaultCommands(
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.setRepeatMode(Player.REPEAT_MODE_ALL);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void setRepeatMode_one_doesNotNotifyAvailableCommandsChanged() {
Player.Commands defaultCommands = createWithDefaultCommands();
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
player.addMediaSource(new FakeMediaSource());
verify(mockListener).onAvailableCommandsChanged(defaultCommands);
player.setRepeatMode(Player.REPEAT_MODE_ONE);
verify(mockListener).onAvailableCommandsChanged(any());
}
@Test
public void setShuffleModeEnabled_notifiesAvailableCommandsChanged() {
Player.Commands commandsWithSeekToPreviousWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
Player.Commands commandsWithSeekToNextWindow =
createWithDefaultCommands(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT);
Player.Listener mockListener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addListener(mockListener);
MediaSource mediaSource =
new ConcatenatingMediaSource(
false,
new FakeShuffleOrder(/* length= */ 2),
new FakeMediaSource(),
new FakeMediaSource());
player.addMediaSource(mediaSource);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow);
// Check that there were no other calls to onAvailableCommandsChanged.
verify(mockListener).onAvailableCommandsChanged(any());
player.setShuffleModeEnabled(true);
verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow);
verify(mockListener, times(2)).onAvailableCommandsChanged(any());
}
@Test
public void
mediaSourceMaybeThrowSourceInfoRefreshError_isNotThrownUntilPlaybackReachedFailingItem()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSource(new FakeMediaSource());
player.addMediaSource(
new FakeMediaSource(/* timeline= */ null) {
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException();
}
});
player.prepare();
player.play();
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
Object period1Uid =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
.uid;
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
}
@Test
public void mediaPeriodMaybeThrowPrepareError_isNotThrownUntilPlaybackReachedFailingItem()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Timeline timeline = new FakeTimeline();
player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.addMediaSource(
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
/* singleSampleTimeUs= */ 0,
mediaSourceEventDispatcher,
DrmSessionManager.DRM_UNSUPPORTED,
drmEventDispatcher,
/* deferOnPrepared= */ true) {
@Override
public void maybeThrowPrepareError() throws IOException {
throw new IOException();
}
};
}
});
player.prepare();
player.play();
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
Object period1Uid =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
.uid;
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
}
@Test
public void sampleStreamMaybeThrowError_isNotThrownUntilPlaybackReachedFailingItem()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Timeline timeline = new FakeTimeline();
player.addMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.addMediaSource(
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
@Override
protected MediaPeriod createMediaPeriod(
MediaPeriodId id,
TrackGroupArray trackGroupArray,
Allocator allocator,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
@Nullable TransferListener transferListener) {
return new FakeMediaPeriod(
trackGroupArray,
allocator,
/* trackDataFactory= */ (format, mediaPeriodId) -> ImmutableList.of(),
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
/* deferOnPrepared= */ false) {
@Override
protected FakeSampleStream createSampleStream(
Allocator allocator,
@Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
Format initialFormat,
List<FakeSampleStreamItem> fakeSampleStreamItems) {
return new FakeSampleStream(
allocator,
mediaSourceEventDispatcher,
drmSessionManager,
drmEventDispatcher,
initialFormat,
fakeSampleStreamItems) {
@Override
public void maybeThrowError() throws IOException {
throw new IOException();
}
};
}
};
}
});
player.prepare();
player.play();
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
Object period1Uid =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
.uid;
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
}
@Test
public void rendererError_isReportedWithReadingMediaPeriodId() throws Exception {
FakeMediaSource source0 =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT);
FakeMediaSource source1 =
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT);
RenderersFactory renderersFactory =
(eventHandler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {
new FakeRenderer(C.TRACK_TYPE_VIDEO),
new FakeRenderer(C.TRACK_TYPE_AUDIO) {
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
// Fail when enabling the renderer. This will happen during the period
// transition while the reading and playing period are different.
throw createRendererException(
new IllegalStateException(),
ExoPlayerTestRunner.AUDIO_FORMAT,
PlaybackException.ERROR_CODE_UNSPECIFIED);
}
}
};
ExoPlayer player =
new TestExoPlayerBuilder(context).setRenderersFactory(renderersFactory).build();
player.setMediaSources(ImmutableList.of(source0, source1));
player.prepare();
player.play();
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
Object period1Uid =
player
.getCurrentTimeline()
.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true)
.uid;
assertThat(error.mediaPeriodId.periodUid).isEqualTo(period1Uid);
// Verify test setup by checking that playing period was indeed different.
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
}
@Test
public void enableOffloadSchedulingWhileIdle_isToggled_isReported() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.experimentalSetOffloadSchedulingEnabled(true);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
}
@Test
public void enableOffloadSchedulingWhilePlaying_isToggled_isReported() throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.prepare();
player.play();
player.experimentalSetOffloadSchedulingEnabled(true);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue();
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
}
@Test
public void enableOffloadSchedulingWhileSleepingForOffload_isDisabled_isReported()
throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare();
player.play();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
player.experimentalSetOffloadSchedulingEnabled(false);
assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse();
}
@Test
public void enableOffloadScheduling_isEnable_playerSleeps() throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO);
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare();
player.play();
sleepRenderer.sleepOnNextRender();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
assertThat(player.experimentalIsSleepingForOffload()).isTrue();
}
@Test
public void
experimentalEnableOffloadSchedulingWhileSleepingForOffload_isDisabled_renderingResumes()
throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare();
player.play();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
player.experimentalSetOffloadSchedulingEnabled(false); // Force the player to exit offload sleep
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false);
assertThat(player.experimentalIsSleepingForOffload()).isFalse();
runUntilPlaybackState(player, Player.STATE_ENDED);
}
@Test
public void wakeupListenerWhileSleepingForOffload_isWokenUp_renderingResumes() throws Exception {
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender();
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(sleepRenderer).build();
Timeline timeline = new FakeTimeline();
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT));
player.experimentalSetOffloadSchedulingEnabled(true);
player.prepare();
player.play();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ true);
sleepRenderer.wakeup();
runUntilSleepingForOffload(player, /* expectedSleepForOffload= */ false);
assertThat(player.experimentalIsSleepingForOffload()).isFalse();
runUntilPlaybackState(player, Player.STATE_ENDED);
}
@Test
public void targetLiveOffsetInMedia_adjustsLiveOffsetToTargetOffset() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(8_900L, 9_100L));
// Assert that none of these playback speed changes were reported.
verify(mockListener, never()).onPlaybackParametersChanged(any());
}
@Test
public void targetLiveOffsetInMedia_withInitialSeek_adjustsLiveOffsetToInitialSeek()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
player.pause();
player.seekTo(18_000);
player.setMediaSource(new FakeMediaSource(timeline), /* resetPosition= */ false);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Target should have been permanently adjusted to 2 seconds.
// (initial now = 20 seconds in live window, initial seek to 18 seconds)
assertThat(liveOffsetAtStart).isIn(Range.closed(1_900L, 2_100L));
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void targetLiveOffsetInMedia_withUserSeek_adjustsLiveOffsetToSeek() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Seek to a live offset of 2 seconds.
player.seekTo(18_000);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert the live offset adjustment was permanent.
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void targetLiveOffsetInMedia_withTimelineUpdate_adjustsLiveOffsetToLatestTimeline()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline initialTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
Timeline updatedTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs + 50_000),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(4_000).build())
.build()));
FakeMediaSource fakeMediaSource = new FakeMediaSource(initialTimeline);
player.pause();
player.setMediaSource(fakeMediaSource);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play a bit and update configuration.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 55_000);
fakeMediaSource.setNewSourceInfo(updatedTimeline);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that adjustment uses target offset from the updated timeline.
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
}
@Test
public void playerIdle_withSetPlaybackSpeed_usesPlaybackParameterSpeedWithPitchUnchanged() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1, /* pitch= */ 2));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.prepare();
player.setPlaybackSpeed(2);
verify(mockListener)
.onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 2, /* pitch= */ 2));
}
@Test
public void setPlaybackSpeed_withAdPlayback_onlyAppliesToContent() throws Exception {
// Create renderer with media clock to listen to playback parameter changes.
ArrayList<PlaybackParameters> playbackParameters = new ArrayList<>();
FakeMediaClockRenderer audioRenderer =
new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) {
private long positionUs;
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
this.positionUs = offsetUs;
}
@Override
public long getPositionUs() {
// Continuously increase position to let playback progress.
positionUs += 10_000;
return positionUs;
}
@Override
public void setPlaybackParameters(PlaybackParameters parameters) {
playbackParameters.add(parameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
return playbackParameters.isEmpty()
? PlaybackParameters.DEFAULT
: Iterables.getLast(playbackParameters);
}
};
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(audioRenderer).build();
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1,
/* adGroupTimesUs...= */ 0,
7 * C.MICROS_PER_SECOND,
C.TIME_END_OF_SOURCE);
TimelineWindowDefinition adTimelineDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(
new FakeMediaSource(
new FakeTimeline(adTimelineDefinition), ExoPlayerTestRunner.AUDIO_FORMAT));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setPlaybackSpeed(5f);
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Assert that the renderer received the playback speed updates at each ad/content boundary.
assertThat(playbackParameters)
.containsExactly(
/* preroll ad */ new PlaybackParameters(1f),
/* content after preroll */ new PlaybackParameters(5f),
/* midroll ad */ new PlaybackParameters(1f),
/* content after midroll */ new PlaybackParameters(5f),
/* postroll ad */ new PlaybackParameters(1f),
/* content after postroll */ new PlaybackParameters(5f))
.inOrder();
// Assert that user-set speed was reported, but none of the ad overrides.
verify(mockListener).onPlaybackParametersChanged(any());
verify(mockListener).onPlaybackParametersChanged(new PlaybackParameters(5.0f));
}
@Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 20 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.pause();
player.setMediaSource(new FakeMediaSource(timeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 20 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(-100L, 100L));
player.setPlaybackParameters(new PlaybackParameters(/* speed */ 2.0f));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that the player didn't adjust the live offset to the media value (9 seconds) and
// instead played the media with double speed (resulting in a negative live offset).
assertThat(liveOffsetAtEnd).isLessThan(0);
// Assert that user-set speed was reported
verify(mockListener).onPlaybackParametersChanged(new PlaybackParameters(2.0f));
}
@Test
public void
targetLiveOffsetInMedia_afterAutomaticPeriodTransition_adjustsLiveOffsetToTargetOffset()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 10_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline nonLiveTimeline = new FakeTimeline();
Timeline liveTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
player.pause();
player.addMediaSource(new FakeMediaSource(nonLiveTimeline));
player.addMediaSource(new FakeMediaSource(liveTimeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(8_900L, 9_100L));
}
@Test
public void
targetLiveOffsetInMedia_afterSeekToDefaultPositionInOtherStream_adjustsLiveOffsetToMediaOffset()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline liveTimeline1 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
Timeline liveTimeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(4_000).build())
.build()));
player.pause();
player.addMediaSource(new FakeMediaSource(liveTimeline1));
player.addMediaSource(new FakeMediaSource(liveTimeline2));
// Ensure we override the target live offset to a seek position in the first live stream.
player.seekTo(10_000);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Seek to default position in second stream.
player.seekToNextMediaItem();
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the media value.
assertThat(liveOffsetAtEnd).isIn(Range.closed(3_900L, 4_100L));
}
@Test
public void
targetLiveOffsetInMedia_afterSeekToSpecificPositionInOtherStream_adjustsLiveOffsetToSeekPosition()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline liveTimeline1 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(9_000).build())
.build()));
Timeline liveTimeline2 =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(4_000).build())
.build()));
player.pause();
player.addMediaSource(new FakeMediaSource(liveTimeline1));
player.addMediaSource(new FakeMediaSource(liveTimeline2));
// Ensure we override the target live offset to a seek position in the first live stream.
player.seekTo(10_000);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Seek to specific position in second stream (at 2 seconds live offset).
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 18_000);
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that player adjusted live offset to the seek.
assertThat(liveOffsetAtEnd).isIn(Range.closed(1_900L, 2_100L));
}
@Test
public void targetLiveOffsetInMedia_unknownWindowStartTime_doesNotAdjustLiveOffset()
throws Exception {
FakeClock fakeClock =
new FakeClock(/* initialTimeMs= */ 987_654_321L, /* isAutoAdvancing= */ true);
ExoPlayer player = new TestExoPlayerBuilder(context).setClock(fakeClock).build();
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(4_000).build())
.build();
Timeline liveTimeline =
new SinglePeriodTimeline(
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* periodDurationUs= */ 1000 * C.MICROS_PER_SECOND,
/* windowDurationUs= */ 1000 * C.MICROS_PER_SECOND,
/* windowPositionInPeriodUs= */ 0,
/* windowDefaultStartPositionUs= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* suppressPositionProjection= */ false,
/* manifest= */ null,
mediaItem,
mediaItem.liveConfiguration);
player.pause();
player.setMediaSource(new FakeMediaSource(liveTimeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long playbackStartTimeMs = fakeClock.elapsedRealtime();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long playbackEndTimeMs = fakeClock.elapsedRealtime();
player.release();
// Assert that the time it took to play 999 seconds of media is 999 seconds (asserting that no
// playback speed adjustment was used).
assertThat(playbackEndTimeMs - playbackStartTimeMs).isEqualTo(999_000);
}
@Test
public void noTargetLiveOffsetInMedia_doesNotAdjustLiveOffset() throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
long nowUnixTimeMs = windowStartUnixTimeMs + 20_000;
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(
new FakeClock(/* initialTimeMs= */ nowUnixTimeMs, /* isAutoAdvancing= */ true))
.build();
Timeline liveTimelineWithoutTargetLiveOffset =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1000 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 8 * C.MICROS_PER_SECOND,
/* windowOffsetInFirstPeriodUs= */ Util.msToUs(windowStartUnixTimeMs),
ImmutableList.of(AdPlaybackState.NONE),
new MediaItem.Builder().setUri(Uri.EMPTY).build()));
player.pause();
player.setMediaSource(new FakeMediaSource(liveTimelineWithoutTargetLiveOffset));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long liveOffsetAtStart = player.getCurrentLiveOffset();
// Verify test setup (now = 20 seconds in live window, default start position = 8 seconds).
assertThat(liveOffsetAtStart).isIn(Range.closed(11_900L, 12_100L));
// Play until close to the end of the available live window.
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 999_000);
long liveOffsetAtEnd = player.getCurrentLiveOffset();
player.release();
// Assert that live offset is still the same (i.e. unadjusted).
assertThat(liveOffsetAtEnd).isIn(Range.closed(11_900L, 12_100L));
}
@Test
public void onEvents_correspondToListenerCalls() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
Format formatWithStaticMetadata =
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_H264)
.setMetadata(
new Metadata(
new BinaryFrame(/* id= */ "", /* data= */ new byte[0]),
new TextInformationFrame(
/* id= */ "TT2", /* description= */ null, /* value= */ "title")))
.build();
// Set multiple values together.
player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formatWithStaticMetadata));
player.seekTo(2_000);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onTimelineChanged(any(), anyInt());
verify(listener).onMediaItemTransition(any(), anyInt());
verify(listener).onPositionDiscontinuity(any(), any(), anyInt());
verify(listener).onPlaybackParametersChanged(any());
ArgumentCaptor<Player.Events> eventCaptor = ArgumentCaptor.forClass(Player.Events.class);
verify(listener).onEvents(eq(player), eventCaptor.capture());
Player.Events events = eventCaptor.getValue();
assertThat(events.contains(Player.EVENT_TIMELINE_CHANGED)).isTrue();
assertThat(events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(events.contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue();
assertThat(events.contains(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue();
// Set values recursively.
player.addListener(
new Player.Listener() {
@Override
public void onRepeatModeChanged(int repeatMode) {
player.setShuffleModeEnabled(true);
}
});
player.setRepeatMode(Player.REPEAT_MODE_ONE);
runUntilPendingCommandsAreFullyHandled(player);
verify(listener).onRepeatModeChanged(anyInt());
verify(listener).onShuffleModeEnabledChanged(anyBoolean());
verify(listener, times(2)).onEvents(eq(player), eventCaptor.capture());
events = Iterables.getLast(eventCaptor.getAllValues());
assertThat(events.contains(Player.EVENT_REPEAT_MODE_CHANGED)).isTrue();
assertThat(events.contains(Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue();
// Ensure all other events are called (even though we can't control how exactly they are
// combined together in onEvents calls).
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
player.play();
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilError(player);
runUntilPendingCommandsAreFullyHandled(player);
player.release();
// Verify that all callbacks have been called at least once.
verify(listener, atLeastOnce()).onTimelineChanged(any(), anyInt());
verify(listener, atLeastOnce()).onMediaItemTransition(any(), anyInt());
verify(listener, atLeastOnce()).onPositionDiscontinuity(any(), any(), anyInt());
verify(listener, atLeastOnce()).onPlaybackParametersChanged(any());
verify(listener, atLeastOnce()).onRepeatModeChanged(anyInt());
verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean());
verify(listener, atLeastOnce()).onPlaybackStateChanged(anyInt());
verify(listener, atLeastOnce()).onIsLoadingChanged(anyBoolean());
verify(listener, atLeastOnce()).onTracksChanged(any(), any());
verify(listener, atLeastOnce()).onMediaMetadataChanged(any());
verify(listener, atLeastOnce()).onPlayWhenReadyChanged(anyBoolean(), anyInt());
verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean());
verify(listener, atLeastOnce()).onPlayerErrorChanged(any());
verify(listener, atLeastOnce()).onPlayerError(any());
// Verify all the same events have been recorded with onEvents.
verify(listener, atLeastOnce()).onEvents(eq(player), eventCaptor.capture());
List<Player.Events> allEvents = eventCaptor.getAllValues();
assertThat(containsEvent(allEvents, Player.EVENT_TIMELINE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_POSITION_DISCONTINUITY)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_REPEAT_MODE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_STATE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_IS_LOADING_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_TRACKS_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_MEDIA_METADATA_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAY_WHEN_READY_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_IS_PLAYING_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYER_ERROR)).isTrue();
}
@Test
public void repeatMode_windowTransition_callsOnPositionDiscontinuityAndOnMediaItemTransition()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
FakeMediaSource secondMediaSource =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 20 * C.MICROS_PER_SECOND)));
player.addListener(listener);
player.setMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))));
player.setRepeatMode(Player.REPEAT_MODE_ONE);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.play();
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.addMediaSource(secondMediaSource);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.TIME_UNSET);
player.play();
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setRepeatMode(Player.REPEAT_MODE_OFF);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
// Expect media item transition for repeat mode ONE to be attributed to
// DISCONTINUITY_REASON_REPEAT.
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
Player.PositionInfo oldPositionInfo = oldPosition.getValue();
Player.PositionInfo newPositionInfo = newPosition.getValue();
assertThat(oldPositionInfo.periodUid).isEqualTo(newPositionInfo.periodUid);
assertThat(oldPositionInfo.periodIndex).isEqualTo(newPositionInfo.periodIndex);
assertThat(oldPositionInfo.mediaItemIndex).isEqualTo(newPositionInfo.mediaItemIndex);
assertThat(oldPositionInfo.mediaItem.localConfiguration.tag).isEqualTo(1);
assertThat(oldPositionInfo.windowUid).isEqualTo(newPositionInfo.windowUid);
assertThat(oldPositionInfo.positionMs).isEqualTo(10_000);
assertThat(oldPositionInfo.contentPositionMs).isEqualTo(10_000);
assertThat(newPositionInfo.positionMs).isEqualTo(0);
assertThat(newPositionInfo.contentPositionMs).isEqualTo(0);
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT));
// Expect media item transition for repeat mode ALL with a single item to be attributed to
// DISCONTINUITY_REASON_REPEAT.
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
oldPositionInfo = oldPosition.getValue();
newPositionInfo = newPosition.getValue();
assertThat(oldPositionInfo.periodUid).isEqualTo(newPositionInfo.periodUid);
assertThat(oldPositionInfo.periodIndex).isEqualTo(newPositionInfo.periodIndex);
assertThat(oldPositionInfo.mediaItemIndex).isEqualTo(newPositionInfo.mediaItemIndex);
assertThat(oldPositionInfo.mediaItem.localConfiguration.tag).isEqualTo(1);
assertThat(oldPositionInfo.windowUid).isEqualTo(newPositionInfo.windowUid);
assertThat(oldPositionInfo.positionMs).isEqualTo(10_000);
assertThat(oldPositionInfo.contentPositionMs).isEqualTo(10_000);
assertThat(newPositionInfo.positionMs).isEqualTo(0);
assertThat(newPositionInfo.contentPositionMs).isEqualTo(0);
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT));
// Expect media item transition for repeat mode ALL with more than one item which is attributed
// to DISCONTINUITY_REASON_AUTO_TRANSITION not DISCONTINUITY_REASON_REPEAT.
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
oldPositionInfo = oldPosition.getValue();
newPositionInfo = newPosition.getValue();
assertThat(oldPositionInfo.mediaItemIndex).isEqualTo(1);
assertThat(oldPositionInfo.mediaItem.localConfiguration.tag).isEqualTo(2);
assertThat(oldPositionInfo.windowUid).isNotEqualTo(newPositionInfo.windowUid);
assertThat(oldPositionInfo.positionMs).isEqualTo(20_000);
assertThat(oldPositionInfo.contentPositionMs).isEqualTo(20_000);
assertThat(newPositionInfo.positionMs).isEqualTo(0);
assertThat(newPositionInfo.mediaItemIndex).isEqualTo(0);
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
// Last auto transition from media item 0 to media item 1 not caused by repeat mode.
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
// No more callbacks called.
inOrder
.verify(listener, never())
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder.verify(listener, never()).onMediaItemTransition(any(), anyInt());
player.release();
}
@Test
public void play_withPreMidAndPostRollAd_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 2,
/* adGroupTimesUs...= */ 0,
7 * C.MICROS_PER_SECOND,
C.TIME_END_OF_SOURCE);
TimelineWindowDefinition adTimeline =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(new FakeMediaSource(new FakeTimeline(adTimeline)));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(
any(), any(), not(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)));
verify(listener, times(8))
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
// first ad group (pre-roll)
// starts with ad to ad transition
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(1);
// ad to content transition
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
// second add group (mid-roll)
assertThat(oldPositions.get(2).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(7000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(2).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(2).positionMs).isEqualTo(0);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(1);
assertThat(newPositions.get(2).adIndexInAdGroup).isEqualTo(0);
// ad to ad transition
assertThat(oldPositions.get(3).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(3).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(3).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(3).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(3).positionMs).isEqualTo(0);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(1);
assertThat(newPositions.get(3).adIndexInAdGroup).isEqualTo(1);
// ad to content transition
assertThat(oldPositions.get(4).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(4).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(4).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(1);
assertThat(oldPositions.get(4).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(4).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(4).positionMs).isEqualTo(7000);
assertThat(newPositions.get(4).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(4).adIndexInAdGroup).isEqualTo(-1);
// third add group (post-roll)
assertThat(oldPositions.get(5).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(5).positionMs).isEqualTo(10000);
assertThat(oldPositions.get(5).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(5).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(5).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(5).positionMs).isEqualTo(0);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(10000);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(2);
assertThat(newPositions.get(5).adIndexInAdGroup).isEqualTo(0);
// ad to ad transition
assertThat(oldPositions.get(6).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(6).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(6).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(6).adGroupIndex).isEqualTo(2);
assertThat(oldPositions.get(6).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(6).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(6).positionMs).isEqualTo(0);
assertThat(newPositions.get(6).contentPositionMs).isEqualTo(10000);
assertThat(newPositions.get(6).adGroupIndex).isEqualTo(2);
assertThat(newPositions.get(6).adIndexInAdGroup).isEqualTo(1);
// post roll ad to end of content transition
assertThat(oldPositions.get(7).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(7).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(7).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(7).adGroupIndex).isEqualTo(2);
assertThat(oldPositions.get(7).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(7).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(7).positionMs).isEqualTo(9999);
assertThat(newPositions.get(7).contentPositionMs).isEqualTo(9999);
assertThat(newPositions.get(7).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(7).adIndexInAdGroup).isEqualTo(-1);
player.release();
}
@Test
public void seekTo_seekOverMidRoll_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 2 * C.MICROS_PER_SECOND);
TimelineWindowDefinition adTimeline =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(new FakeMediaSource(new FakeTimeline(adTimeline)));
player.prepare();
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 1000);
player.seekTo(/* positionMs= */ 8_000);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
verify(listener, never())
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener, never())
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SKIP));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// SEEK behind mid roll
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(980L, 1_000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(980L, 1_000L));
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(8_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(8_000);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
// SEEK_ADJUSTMENT back to ad
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(8_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(8_000);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(8000);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
// AUTO_TRANSITION back to content
assertThat(oldPositions.get(2).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(5_000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(8_000);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(2).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(2).positionMs).isEqualTo(8_000);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(8_000);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).adIndexInAdGroup).isEqualTo(-1);
player.release();
}
@Test
public void play_multiItemPlaylistWidthAds_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
AdPlaybackState postRollAdPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE);
TimelineWindowDefinition postRollWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ "id-2",
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 20 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
postRollAdPlaybackState);
AdPlaybackState preRollAdPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 0);
TimelineWindowDefinition preRollWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ "id-3",
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 25 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
preRollAdPlaybackState);
player.setMediaSources(
ImmutableList.of(
createFakeMediaSource(/* id= */ "id-0"),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ "id-1",
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 15 * C.MICROS_PER_SECOND))),
new FakeMediaSource(new FakeTimeline(postRollWindow)),
new FakeMediaSource(new FakeTimeline(preRollWindow))));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
Window window = new Window();
InOrder inOrder = Mockito.inOrder(listener);
// from first to second media item
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
assertThat(oldPosition.getValue().windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(0, window).uid);
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(0);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-0");
assertThat(oldPosition.getValue().positionMs).isEqualTo(10_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(10_000);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
assertThat(newPosition.getValue().windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(1, window).uid);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(1);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-1");
assertThat(newPosition.getValue().positionMs).isEqualTo(0);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(0);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
// from second media item to third
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
assertThat(oldPosition.getValue().windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(1, window).uid);
assertThat(newPosition.getValue().windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(2, window).uid);
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(1);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-1");
assertThat(oldPosition.getValue().positionMs).isEqualTo(15_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(15_000);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(newPosition.getValue().positionMs).isEqualTo(0);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(0);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
// from third media item content to post roll ad
@Nullable Object lastNewWindowUid = newPosition.getValue().windowUid;
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(oldPosition.getValue().windowUid).isEqualTo(lastNewWindowUid);
assertThat(oldPosition.getValue().positionMs).isEqualTo(20_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(20_000);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(newPosition.getValue().positionMs).isEqualTo(0);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(20_000);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(0);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(0);
// from third media item post roll to third media item content end
lastNewWindowUid = newPosition.getValue().windowUid;
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(oldPosition.getValue().windowUid).isEqualTo(lastNewWindowUid);
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(oldPosition.getValue().positionMs).isEqualTo(5_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(20_000);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(0);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(0);
assertThat(newPosition.getValue().windowUid).isEqualTo(oldPosition.getValue().windowUid);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(newPosition.getValue().positionMs).isEqualTo(19_999);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(19_999);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
// from third media item content end to fourth media item pre roll ad
lastNewWindowUid = newPosition.getValue().windowUid;
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
assertThat(oldPosition.getValue().windowUid).isEqualTo(lastNewWindowUid);
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(2);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-2");
assertThat(oldPosition.getValue().positionMs).isEqualTo(20_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(20_000);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
assertThat(newPosition.getValue().windowUid).isNotEqualTo(oldPosition.getValue().windowUid);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(3);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-3");
assertThat(newPosition.getValue().positionMs).isEqualTo(0);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(0);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(0);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(0);
// from fourth media item pre roll ad to fourth media item content
lastNewWindowUid = newPosition.getValue().windowUid;
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(oldPosition.getValue().windowUid).isEqualTo(lastNewWindowUid);
assertThat(oldPosition.getValue().mediaItemIndex).isEqualTo(3);
assertThat(oldPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-3");
assertThat(oldPosition.getValue().positionMs).isEqualTo(5_000);
assertThat(oldPosition.getValue().contentPositionMs).isEqualTo(0);
assertThat(oldPosition.getValue().adGroupIndex).isEqualTo(0);
assertThat(oldPosition.getValue().adIndexInAdGroup).isEqualTo(0);
assertThat(newPosition.getValue().windowUid).isEqualTo(oldPosition.getValue().windowUid);
assertThat(newPosition.getValue().mediaItemIndex).isEqualTo(3);
assertThat(newPosition.getValue().mediaItem.localConfiguration.tag).isEqualTo("id-3");
assertThat(newPosition.getValue().positionMs).isEqualTo(0);
assertThat(newPosition.getValue().contentPositionMs).isEqualTo(0);
assertThat(newPosition.getValue().adGroupIndex).isEqualTo(-1);
assertThat(newPosition.getValue().adIndexInAdGroup).isEqualTo(-1);
inOrder
.verify(listener, never())
.onPositionDiscontinuity(
any(), any(), not(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)));
inOrder
.verify(listener, never())
.onMediaItemTransition(any(), not(eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)));
player.release();
}
@Test
public void setMediaSources_removesPlayingPeriod_callsOnPositionDiscontinuity() throws Exception {
FakeMediaSource secondMediaSource =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 15 * C.MICROS_PER_SECOND)));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.setMediaSources(ImmutableList.of(secondMediaSource, secondMediaSource));
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void onPositionDiscontinuity_recursiveStateChange_mediaItemMaskingCorrect()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
MediaItem[] currentMediaItems = new MediaItem[2];
int[] mediaItemCount = new int[2];
player.addListener(
new Listener() {
@Override
public void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
mediaItemCount[0] = player.getMediaItemCount();
currentMediaItems[0] = player.getCurrentMediaItem();
// This is called before the second listener is called.
player.removeMediaItem(/* index= */ 1);
}
}
});
player.addListener(
new Listener() {
@Override
public void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, int reason) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
mediaItemCount[1] = player.getMediaItemCount();
currentMediaItems[1] = player.getCurrentMediaItem();
}
}
});
player.addListener(listener);
player.setMediaSources(
ImmutableList.of(
createFakeMediaSource(/* id= */ "id-0"), createFakeMediaSource(/* id= */ "id-1")));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<PositionInfo> newPositionArgumentCaptor =
ArgumentCaptor.forClass(PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(
any(),
newPositionArgumentCaptor.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onPositionDiscontinuity(
any(), newPositionArgumentCaptor.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
// The state at auto-transition event time.
assertThat(mediaItemCount[0]).isEqualTo(2);
assertThat(currentMediaItems[0].localConfiguration.tag).isEqualTo("id-1");
// The masked state after id-1 has been removed.
assertThat(mediaItemCount[1]).isEqualTo(1);
assertThat(currentMediaItems[1].localConfiguration.tag).isEqualTo("id-0");
// PositionInfo reports the media item at event time.
assertThat(newPositionArgumentCaptor.getAllValues().get(0).mediaItem.localConfiguration.tag)
.isEqualTo("id-1");
assertThat(newPositionArgumentCaptor.getAllValues().get(1).mediaItem.localConfiguration.tag)
.isEqualTo("id-0");
player.release();
}
@Test
public void removeMediaItems_removesPlayingPeriod_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND)))));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.removeMediaItem(/* index= */ 1);
player.seekTo(/* positionMs= */ 0);
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 2 * C.MILLIS_PER_SECOND);
// Removing the last item resets the position to 0 with an empty timeline.
player.removeMediaItem(0);
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isIn(Range.closed(1980L, 2000L));
assertThat(oldPositions.get(1).contentPositionMs).isIn(Range.closed(1980L, 2000L));
assertThat(newPositions.get(1).windowUid).isNull();
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void
concatenatingMediaSourceRemoveMediaSource_removesPlayingPeriod_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 6 * C.MICROS_PER_SECOND))));
player.addMediaSource(concatenatingMediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
concatenatingMediaSource.removeMediaSource(1);
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
concatenatingMediaSource.removeMediaSource(1);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(0);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void
concatenatingMediaSourceRemoveMediaSourceWithSeek_overridesRemoval_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 6 * C.MICROS_PER_SECOND))));
player.addMediaSource(concatenatingMediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
concatenatingMediaSource.removeMediaSource(1);
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234);
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
concatenatingMediaSource.removeMediaSource(0);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
// SEEK overrides concatenating media source modification.
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
// This fails once out of a hundred test runs due to a race condition whether the seek or the
// removal arrives first in EPI.
// inOrder.verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(1234);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(1234);
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(1234);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(1234);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(1234);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1234);
player.release();
}
@Test
public void setMediaItems_callsListenersWithSameInstanceOfMediaItem() throws Exception {
ArgumentCaptor<Timeline> timeline = ArgumentCaptor.forClass(Timeline.class);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<MediaItem> currentMediaItem = ArgumentCaptor.forClass(MediaItem.class);
Window window = new Timeline.Window();
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setMediaSourceFactory(new FakeMediaSourceFactory())
.build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
List<MediaItem> playlist =
ImmutableList.of(
MediaItem.fromUri("http://item-0.com/"), MediaItem.fromUri("http://item-1.com/"));
player.setMediaItems(playlist);
player.prepare();
player.seekTo(/* positionMs= */ 2000);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onTimelineChanged(timeline.capture(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onMediaItemTransition(
currentMediaItem.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onMediaItemTransition(
currentMediaItem.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO));
inOrder.verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
inOrder.verify(listener, never()).onMediaItemTransition(any(), anyInt());
assertThat(timeline.getValue().getWindow(0, window).mediaItem)
.isSameInstanceAs(playlist.get(0));
assertThat(timeline.getValue().getWindow(1, window).mediaItem)
.isSameInstanceAs(playlist.get(1));
assertThat(oldPosition.getAllValues().get(0).mediaItem).isSameInstanceAs(playlist.get(0));
assertThat(newPosition.getAllValues().get(0).mediaItem).isSameInstanceAs(playlist.get(0));
assertThat(oldPosition.getAllValues().get(1).mediaItem).isSameInstanceAs(playlist.get(0));
assertThat(newPosition.getAllValues().get(1).mediaItem).isSameInstanceAs(playlist.get(1));
assertThat(currentMediaItem.getAllValues().get(0)).isSameInstanceAs(playlist.get(0));
assertThat(currentMediaItem.getAllValues().get(1)).isSameInstanceAs(playlist.get(1));
player.release();
}
@Test
public void seekTo_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSources(
ImmutableList.of(
createFakeMediaSource(/* id= */ "id-0"), createFakeMediaSource(/* id= */ "id-1")));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.seekTo(/* positionMs= */ 7 * C.MILLIS_PER_SECOND);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.MILLIS_PER_SECOND);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowUid).isEqualTo(newPositions.get(0).windowUid);
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).mediaItem.localConfiguration.tag).isEqualTo("id-0");
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).mediaItem.localConfiguration.tag).isEqualTo("id-0");
assertThat(newPositions.get(0).positionMs).isEqualTo(7_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).windowUid).isNotEqualTo(newPositions.get(1).windowUid);
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).mediaItem.localConfiguration.tag).isEqualTo("id-0");
assertThat(oldPositions.get(1).positionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(7_000);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(1);
assertThat(newPositions.get(1).mediaItem.localConfiguration.tag).isEqualTo("id-1");
assertThat(newPositions.get(1).positionMs).isEqualTo(1_000);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1_000);
player.release();
}
@Test
public void seekTo_whenTimelineEmpty_callsOnPositionDiscontinuity() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.seekTo(/* positionMs= */ 7 * C.MILLIS_PER_SECOND);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ C.MILLIS_PER_SECOND);
player.seekTo(/* positionMs= */ 5 * C.MILLIS_PER_SECOND);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener, times(3))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// a seek from initial state to masked seek position
assertThat(oldPositions.get(0).windowUid).isNull();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).mediaItem).isNull();
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).windowUid).isNull();
assertThat(newPositions.get(0).positionMs).isEqualTo(7_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(7_000);
// a seek from masked seek position to another masked position across windows
assertThat(oldPositions.get(1).windowUid).isNull();
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(1).mediaItem).isNull();
assertThat(oldPositions.get(1).positionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(7_000);
assertThat(newPositions.get(1).windowUid).isNull();
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1_000);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1_000);
// a seek from masked seek position to another masked position within media item
assertThat(oldPositions.get(2).windowUid).isNull();
assertThat(oldPositions.get(2).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(2).mediaItem).isNull();
assertThat(oldPositions.get(2).positionMs).isEqualTo(1_000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(1_000);
assertThat(newPositions.get(2).windowUid).isNull();
assertThat(newPositions.get(2).mediaItemIndex).isEqualTo(1);
assertThat(newPositions.get(2).positionMs).isEqualTo(5_000);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(5_000);
player.release();
}
@Test
public void seekBack_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ Util.msToUs(3 * C.DEFAULT_SEEK_BACK_INCREMENT_MS)));
player.setMediaSource(new FakeMediaSource(fakeTimeline));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS);
player.seekBack();
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs)
.isIn(
Range.closed(
2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS - 20, 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS));
assertThat(oldPositions.get(0).contentPositionMs)
.isIn(
Range.closed(
2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS - 20, 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs)
.isIn(
Range.closed(C.DEFAULT_SEEK_BACK_INCREMENT_MS - 20, C.DEFAULT_SEEK_BACK_INCREMENT_MS));
assertThat(newPositions.get(0).contentPositionMs)
.isIn(
Range.closed(C.DEFAULT_SEEK_BACK_INCREMENT_MS - 20, C.DEFAULT_SEEK_BACK_INCREMENT_MS));
player.release();
}
@Test
public void seekBack_pastZero_seeksToZero() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ Util.msToUs(C.DEFAULT_SEEK_BACK_INCREMENT_MS)));
player.setMediaSource(new FakeMediaSource(fakeTimeline));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS / 2);
player.seekBack();
assertThat(player.getCurrentPosition()).isEqualTo(0);
player.release();
}
@Test
public void seekForward_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ Util.msToUs(2 * C.DEFAULT_SEEK_FORWARD_INCREMENT_MS)));
player.setMediaSource(new FakeMediaSource(fakeTimeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
player.seekForward();
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(C.DEFAULT_SEEK_FORWARD_INCREMENT_MS);
assertThat(newPositions.get(0).contentPositionMs)
.isEqualTo(C.DEFAULT_SEEK_FORWARD_INCREMENT_MS);
player.release();
}
@Test
public void seekForward_pastDuration_seeksToDuration() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ Util.msToUs(C.DEFAULT_SEEK_FORWARD_INCREMENT_MS / 2)));
player.setMediaSource(new FakeMediaSource(fakeTimeline));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
player.seekForward();
assertThat(player.getCurrentPosition()).isEqualTo(C.DEFAULT_SEEK_FORWARD_INCREMENT_MS / 2 - 1);
player.release();
}
@Test
public void seekToPrevious_withPreviousWindowAndCloseToStart_seeksToPreviousWindow() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
player.seekTo(/* mediaItemIndex= */ 1, C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS);
player.seekToPrevious();
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(player.getCurrentPosition()).isEqualTo(0);
player.release();
}
@Test
public void seekToPrevious_notCloseToStart_seeksToZero() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
player.seekTo(/* mediaItemIndex= */ 1, C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS + 1);
player.seekToPrevious();
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentPosition()).isEqualTo(0);
player.release();
}
@Test
public void seekToNext_withNextWindow_seeksToNextWindow() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.addMediaSources(ImmutableList.of(new FakeMediaSource(), new FakeMediaSource()));
player.seekToNext();
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1);
assertThat(player.getCurrentPosition()).isEqualTo(0);
player.release();
}
@Test
public void seekToNext_liveWindowWithoutNextWindow_seeksToLiveEdge() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Timeline timeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* isLive= */ true,
/* isPlaceholder= */ false,
/* durationUs= */ 1_000_000,
/* defaultPositionUs= */ 500_000,
TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US,
AdPlaybackState.NONE));
MediaSource mediaSource = new FakeMediaSource(timeline);
player.setMediaSource(mediaSource);
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
player.seekTo(/* positionMs = */ 0);
player.seekToNext();
assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
assertThat(player.getCurrentPosition()).isEqualTo(500);
player.release();
}
@Test
public void stop_doesNotCallOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.stop();
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
player.release();
}
// Tests deprecated stop(boolean reset)
@SuppressWarnings("deprecation")
@Test
public void stop_withResetRemovesPlayingPeriod_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(createFakeMediaSource(/* id= */ 123));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.stop(/* reset= */ true);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_REMOVE)));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(oldPositions.get(0).mediaItem.localConfiguration.tag).isEqualTo(123);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowUid).isNull();
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(0).mediaItem).isNull();
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void seekTo_cancelsSourceDiscontinuity_callsOnPositionDiscontinuity() throws Exception {
Timeline timeline1 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2));
final Timeline timeline2 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 1, /* positionMs= */ 2000);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 2122);
// This causes a DISCONTINUITY_REASON_REMOVE between pending operations that needs to be
// cancelled by the seek below.
mediaSource.setNewSourceInfo(timeline2);
player.play();
player.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 2222);
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder.verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// First seek
assertThat(oldPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(1980L, 2000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(1980L, 2000L));
assertThat(newPositions.get(0).mediaItemIndex).isEqualTo(1);
assertThat(newPositions.get(0).positionMs).isEqualTo(2122);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(2122);
// Second seek.
assertThat(oldPositions.get(1).mediaItemIndex).isEqualTo(1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(2122);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(2122);
assertThat(newPositions.get(1).mediaItemIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(2222);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(2222);
player.release();
}
@Test
public void newServerSideInsertedAdAtPlaybackPosition_keepsRenderersEnabled() throws Exception {
// Injecting renderer to count number of renderer resets.
AtomicReference<FakeVideoRenderer> videoRenderer = new AtomicReference<>();
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> {
videoRenderer.set(new FakeVideoRenderer(handler, videoListener));
return new Renderer[] {videoRenderer.get()};
})
.build();
// Live stream timeline with unassigned next ad group.
AdPlaybackState initialAdPlaybackState =
new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdDurationsUs(new long[][] {new long[] {10 * C.MICROS_PER_SECOND}});
// Updated timeline with ad group at 18 seconds.
long firstSampleTimeUs = TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US;
Timeline initialTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
initialAdPlaybackState));
AdPlaybackState updatedAdPlaybackState =
initialAdPlaybackState.withAdGroupTimeUs(
/* adGroupIndex= */ 0,
/* adGroupTimeUs= */ firstSampleTimeUs + 18 * C.MICROS_PER_SECOND);
Timeline updatedTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ true,
/* durationUs= */ C.TIME_UNSET,
updatedAdPlaybackState));
// Add samples to allow player to load and start playing (but no EOS as this is a live stream).
FakeMediaSource mediaSource =
new FakeMediaSource(
initialTimeline,
DrmSessionManager.DRM_UNSUPPORTED,
(format, mediaPeriodId) ->
ImmutableList.of(
oneByteSample(firstSampleTimeUs, C.BUFFER_FLAG_KEY_FRAME),
oneByteSample(firstSampleTimeUs + 40 * C.MICROS_PER_SECOND)),
ExoPlayerTestRunner.VIDEO_FORMAT);
// Set updated ad group once we reach 20 seconds, and then continue playing until 40 seconds.
player
.createMessage((message, payload) -> mediaSource.setNewSourceInfo(updatedTimeline))
.setPosition(20_000)
.send();
player.setMediaSource(mediaSource);
player.prepare();
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 40_000);
player.release();
// Assert that the renderer hasn't been reset despite the inserted ad group.
assertThat(videoRenderer.get().positionResetCount).isEqualTo(1);
}
@Test
public void setMediaItem_withMediaMetadata_updatesMediaMetadata() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build();
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaItem(
new MediaItem.Builder()
.setMediaId("id")
.setUri(Uri.EMPTY)
.setMediaMetadata(mediaMetadata)
.build());
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
}
@Test
public void playingMedia_withNoMetadata_doesNotUpdateMediaMetadata() throws Exception {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("the title").build();
ExoPlayer player = new TestExoPlayerBuilder(context).build();
MediaItem mediaItem =
new MediaItem.Builder()
.setMediaId("id")
.setUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"))
.setMediaMetadata(mediaMetadata)
.build();
player.setMediaItem(mediaItem);
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.stop();
shadowOf(Looper.getMainLooper()).idle();
assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata);
}
@Test
@Config(sdk = Config.ALL_SDKS)
public void builder_inBackgroundThread_doesNotThrow() throws Exception {
Thread builderThread =
new Thread(
() -> new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build());
AtomicReference<Throwable> builderThrow = new AtomicReference<>();
builderThread.setUncaughtExceptionHandler((thread, throwable) -> builderThrow.set(throwable));
builderThread.start();
builderThread.join();
assertThat(builderThrow.get()).isNull();
}
@Test
public void onPlaylistMetadataChanged_calledWhenPlaylistMetadataSet() {
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
Player.Listener playerListener = mock(Player.Listener.class);
player.addListener(playerListener);
AnalyticsListener analyticsListener = mock(AnalyticsListener.class);
player.addAnalyticsListener(analyticsListener);
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("test").build();
player.setPlaylistMetadata(mediaMetadata);
verify(playerListener).onPlaylistMetadataChanged(mediaMetadata);
verify(analyticsListener).onPlaylistMetadataChanged(any(), eq(mediaMetadata));
}
@Test
public void release_triggersAllPendingEventsInAnalyticsListeners() throws Exception {
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
.build();
AnalyticsListener listener = mock(AnalyticsListener.class);
player.addAnalyticsListener(listener);
// Do something that requires clean-up callbacks like decoder disabling.
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener).onVideoDisabled(any(), any());
verify(listener).onPlayerReleased(any());
}
@Test
public void releaseAfterRendererEvents_triggersPendingVideoEventsInListener() throws Exception {
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
.build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
player.setVideoSurface(surface);
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
player.release();
surface.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener, atLeastOnce()).onEvents(any(), any()); // EventListener
verify(listener).onRenderedFirstFrame(); // VideoListener
}
@Test
public void releaseAfterVolumeChanges_triggerPendingVolumeEventInListener() throws Exception {
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setVolume(0F);
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener).onVolumeChanged(anyFloat());
}
@Test
public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener() {
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
int deviceVolume = player.getDeviceVolume();
try {
player.setDeviceVolume(deviceVolume + 1); // No-op if at max volume.
player.setDeviceVolume(deviceVolume - 1); // No-op if at min volume.
} finally {
player.setDeviceVolume(deviceVolume); // Restore original volume.
}
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener, atLeast(2)).onDeviceVolumeChanged(anyInt(), anyBoolean());
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
final Surface surface1 = new Surface(new SurfaceTexture(/* texName= */ 0));
final Surface surface2 = new Surface(new SurfaceTexture(/* texName= */ 1));
return builder
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.setVideoSurface(surface1);
}
})
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.setVideoSurface(surface2);
}
});
}
private static FakeMediaSource createFakeMediaSource(Object id) {
return new FakeMediaSource(
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, id)));
}
private static void deliverBroadcast(Intent intent) {
ApplicationProvider.getApplicationContext().sendBroadcast(intent);
shadowOf(Looper.getMainLooper()).idle();
}
private static boolean containsEvent(List<Player.Events> eventsList, @Player.Event int event) {
for (Player.Events events : eventsList) {
if (events.contains(event)) {
return true;
}
}
return false;
}
private static Player.Commands createWithDefaultCommands(
boolean isTimelineEmpty, @Player.Command int... additionalCommands) {
Player.Commands.Builder builder = new Player.Commands.Builder();
builder.addAll(
COMMAND_PLAY_PAUSE,
COMMAND_PREPARE,
COMMAND_STOP,
COMMAND_SEEK_TO_DEFAULT_POSITION,
COMMAND_SEEK_TO_MEDIA_ITEM,
COMMAND_SET_SPEED_AND_PITCH,
COMMAND_SET_SHUFFLE_MODE,
COMMAND_SET_REPEAT_MODE,
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_VOLUME,
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS);
if (!isTimelineEmpty) {
builder.add(COMMAND_SEEK_TO_PREVIOUS);
}
builder.addAll(additionalCommands);
return builder.build();
}
private static Player.Commands createWithDefaultCommands(
@Player.Command int... additionalCommands) {
return createWithDefaultCommands(/* isTimelineEmpty= */ false, additionalCommands);
}
// Internal classes.
/** {@link FakeRenderer} that can sleep and be woken-up. */
private static class FakeSleepRenderer extends FakeRenderer {
private static final long WAKEUP_DEADLINE_MS = 60 * C.MICROS_PER_SECOND;
private final AtomicBoolean sleepOnNextRender;
private final AtomicReference<Renderer.WakeupListener> wakeupListenerReceiver;
public FakeSleepRenderer(int trackType) {
super(trackType);
sleepOnNextRender = new AtomicBoolean(false);
wakeupListenerReceiver = new AtomicReference<>();
}
public void wakeup() {
wakeupListenerReceiver.get().onWakeup();
}
/**
* Call {@link Renderer.WakeupListener#onSleep(long)} on the next {@link #render(long, long)}
*/
public FakeSleepRenderer sleepOnNextRender() {
sleepOnNextRender.set(true);
return this;
}
@Override
public void handleMessage(@MessageType int messageType, @Nullable Object message)
throws ExoPlaybackException {
if (messageType == MSG_SET_WAKEUP_LISTENER) {
assertThat(message).isNotNull();
wakeupListenerReceiver.set((WakeupListener) message);
}
super.handleMessage(messageType, message);
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
super.render(positionUs, elapsedRealtimeUs);
if (sleepOnNextRender.compareAndSet(/* expectedValue= */ true, /* newValue= */ false)) {
wakeupListenerReceiver.get().onSleep(WAKEUP_DEADLINE_MS);
}
}
}
private static final class CountingMessageTarget implements PlayerMessage.Target {
public int messageCount;
@Override
public void handleMessage(@Renderer.MessageType int messageType, @Nullable Object message) {
messageCount++;
}
}
private static final class PositionGrabbingMessageTarget extends PlayerTarget {
public int mediaItemIndex;
public long positionMs;
public int messageCount;
public PositionGrabbingMessageTarget() {
mediaItemIndex = C.INDEX_UNSET;
positionMs = C.POSITION_UNSET;
}
@Override
public void handleMessage(ExoPlayer player, int messageType, @Nullable Object message) {
mediaItemIndex = player.getCurrentMediaItemIndex();
positionMs = player.getCurrentPosition();
messageCount++;
}
}
private static final class PlayerStateGrabber extends PlayerRunnable {
public boolean playWhenReady;
public @Player.State int playbackState;
@Nullable public Timeline timeline;
@Override
public void run(ExoPlayer player) {
playWhenReady = player.getPlayWhenReady();
playbackState = player.getPlaybackState();
timeline = player.getCurrentTimeline();
}
}
/**
* Provides a wrapper for a {@link Runnable} which does collect playback states and window counts.
* Can be used with {@link ActionSchedule.Builder#executeRunnable(Runnable)} to verify that a
* playback state did not change and hence no observable callback is called.
*
* <p>This is specifically useful in cases when the test may end before a given state arrives or
* when an action of the action schedule might execute before a callback is called.
*/
public static class PlaybackStateCollector extends PlayerRunnable {
private final int[] playbackStates;
private final int[] timelineWindowCount;
private final int index;
/**
* Creates the collector.
*
* @param index The index to populate.
* @param playbackStates An array of playback states to populate.
* @param timelineWindowCount An array of window counts to populate.
*/
public PlaybackStateCollector(int index, int[] playbackStates, int[] timelineWindowCount) {
Assertions.checkArgument(playbackStates.length > index && timelineWindowCount.length > index);
this.playbackStates = playbackStates;
this.timelineWindowCount = timelineWindowCount;
this.index = index;
}
@Override
public void run(ExoPlayer player) {
playbackStates[index] = player.getPlaybackState();
timelineWindowCount[index] = player.getCurrentTimeline().getWindowCount();
}
}
private static final class FakeLoaderCallback implements Loader.Callback<Loader.Loadable> {
@Override
public void onLoadCompleted(
Loader.Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) {}
@Override
public void onLoadCanceled(
Loader.Loadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) {}
@Override
public Loader.LoadErrorAction onLoadError(
Loader.Loadable loadable,
long elapsedRealtimeMs,
long loadDurationMs,
IOException error,
int errorCount) {
return Loader.RETRY;
}
}
/**
* Returns an argument matcher for {@link Timeline} instances that ignores period and window uids.
*/
private static ArgumentMatcher<Timeline> noUid(Timeline timeline) {
return argument -> new NoUidTimeline(timeline).equals(new NoUidTimeline(argument));
}
}