mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +00:00
Ignorable ad periods are skipped to resolve the media period id with the ad playback state of the resulting period. In case of a change in the period position un-played ad periods are rolled forward to be played. PiperOrigin-RevId: 428011116
12250 lines
534 KiB
Java
12250 lines
534 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.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
|
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();
|
|
|
|
// There is still one discontinuity from content to content for the failed ad insertion.
|
|
PositionInfo positionInfo =
|
|
new PositionInfo(
|
|
window.uid,
|
|
/* mediaItemIndex= */ 0,
|
|
window.mediaItem,
|
|
period.uid,
|
|
/* periodIndex= */ 0,
|
|
/* positionMs= */ 5_000,
|
|
/* contentPositionMs= */ 5_000,
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
verify(mockListener)
|
|
.onPositionDiscontinuity(
|
|
positionInfo, positionInfo, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
|
|
}
|
|
|
|
@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 is still one discontinuity from content to content for the failed ad insertion and the
|
|
// normal ad transition for the successful ad insertion.
|
|
PositionInfo positionInfoFailedAd =
|
|
new PositionInfo(
|
|
window.uid,
|
|
/* mediaItemIndex= */ 0,
|
|
window.mediaItem,
|
|
period.uid,
|
|
/* periodIndex= */ 0,
|
|
/* positionMs= */ 5_000,
|
|
/* contentPositionMs= */ 5_000,
|
|
/* adGroupIndex= */ C.INDEX_UNSET,
|
|
/* adIndexInAdGroup= */ C.INDEX_UNSET);
|
|
verify(mockListener)
|
|
.onPositionDiscontinuity(
|
|
positionInfoFailedAd,
|
|
positionInfoFailedAd,
|
|
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
|
|
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());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
|
|
// 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());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(1, 2, 0, 0, 0).inOrder();
|
|
// 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());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(1, 0, 0, 0, 0, 0).inOrder();
|
|
// 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());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(1, 2, 0, 0, 0, 0).inOrder();
|
|
// 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());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(1).inOrder();
|
|
// 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_playedSSAIPreMidPostRolls_skipsAllAds() 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),
|
|
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();
|
|
|
|
verify(listener, times(3))
|
|
.onPositionDiscontinuity(
|
|
oldPositionArgumentCaptor.capture(),
|
|
newPositionArgumentCaptor.capture(),
|
|
reasonArgumentCaptor.capture());
|
|
List<PositionInfo> oldPositions = oldPositionArgumentCaptor.getAllValues();
|
|
List<PositionInfo> newPositions = newPositionArgumentCaptor.getAllValues();
|
|
List<Integer> reasons = reasonArgumentCaptor.getAllValues();
|
|
assertThat(reasons).containsExactly(0, 0, 0).inOrder();
|
|
// Auto discontinuity from the empty ad 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 postroll.
|
|
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 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).getTrackGroup().getFormat(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));
|
|
}
|
|
}
|