Migrate ExoPlayerTest to Robolectric.

So far this wasn't possible because Robolectric's Looper and MessageQueue
implementations have multiple shortcomings:
 1. The message loop of new HandlerThreads is an not an actual loop and
    scheduled messages are executed on the thread the message is enqueued
    (not the handler thread).
 2. The scheduler used to replace the message queue is synchronizing all its
    methods. Thus, when a test attempts to add messages to a Handler from
    two different threads, it may easily run into a deadlock.
 3. The scheduler doesn't correctly emulate the order of messages as they
    would be in an actual MessageQueue:
   a. If the message is enqueued on the handler thread, it gets executed
      immediately (and not after all other messages at the same time).
   b. The list of messages is always re-sorted by time, meaning that the
      order of execution for messages at the same time is indeterminate.
 4. Robolectric's SystemClock implementation returns the current scheduler
    time of the main UI thread. So, unless this scheduler is used to add
    messages in the future, the SystemClock time never advances.

This CL adds two helper classes which extend and replace Robolectric's
ShadowLooper and ShadowMessageQueue.
 1. We intercept messages being enqueued or deleted in the message queue.
    Thus Robolectric's faulty scheduler gets never used. Instead, we keep
    a blocking priority queue of messages, sorted first by execution time
    and then by FIFO order to correctly emulate the real MessageQueue.
 2. We also keep a list of deleted messages to know which messages to ignore
    when they come up in the looper.
 3. When a new Looper is started, we override the dummy loop to an actual
    eternal while loop which waits for new messages, checks if they haven't
    been deleted, and runs the messages (similar to what Robolectric's
    MessageQueue would have done at this point).

Because we don't actually use the main UI thread in our tests, we can't rely
on the SystemClock to progress in any sensible manner. To overcome this issue,
we can use the auto-advancing FakeClock also used for the simulation tests.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182912510
This commit is contained in:
tonihei 2018-01-23 04:00:15 -08:00 committed by Oliver Woodman
parent e991a8015b
commit 4671c23c4d
4 changed files with 585 additions and 214 deletions

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.view.Surface;
import com.google.android.exoplayer2.Player.DefaultEventListener;
@ -40,18 +41,24 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit
import com.google.android.exoplayer2.testutil.FakeTrackSelection;
import com.google.android.exoplayer2.testutil.FakeTrackSelector;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.video.DummySurface;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import junit.framework.TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/**
* Unit test for {@link ExoPlayer}.
*/
public final class ExoPlayerTest extends TestCase {
/** Unit test for {@link ExoPlayer}. */
@RunWith(RobolectricTestRunner.class)
@Config(
sdk = Config.TARGET_SDK,
manifest = Config.NONE,
shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}
)
public final class ExoPlayerTest {
/**
* For tests that rely on the player transitioning to the ended state, the duration in
@ -64,6 +71,7 @@ public final class ExoPlayerTest extends TestCase {
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
* error.
*/
@Test
public void testPlayEmptyTimeline() throws Exception {
Timeline timeline = Timeline.EMPTY;
FakeRenderer renderer = new FakeRenderer();
@ -81,9 +89,8 @@ public final class ExoPlayerTest extends TestCase {
assertThat(renderer.isEnded).isFalse();
}
/**
* Tests playback of a source that exposes a single period.
*/
/** Tests playback of a source that exposes a single period. */
@Test
public void testPlaySinglePeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Object manifest = new Object();
@ -106,9 +113,8 @@ public final class ExoPlayerTest extends TestCase {
assertThat(renderer.isEnded).isTrue();
}
/**
* Tests playback of a source that exposes three periods.
*/
/** Tests playback of a source that exposes three periods. */
@Test
public void testPlayMultiPeriodTimeline() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
@ -130,6 +136,7 @@ public final class ExoPlayerTest extends TestCase {
}
/** Tests playback of periods with very short duration. */
@Test
public void testPlayShortDurationPeriods() throws Exception {
// TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period.
Timeline timeline =
@ -156,16 +163,19 @@ public final class ExoPlayerTest extends TestCase {
* Tests that the player does not unnecessarily reset renderers when playing a multi-period
* source.
*/
@Test
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
FakeMediaClockRenderer audioRenderer =
new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
@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.
// 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.
// TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US.
return isCurrentStreamFinal() ? 60000030 : 60000000;
}
@ -184,7 +194,6 @@ public final class ExoPlayerTest extends TestCase {
public boolean isEnded() {
return videoRenderer.isEnded();
}
};
ExoPlayerTestRunner testRunner =
new Builder()
@ -203,12 +212,13 @@ public final class ExoPlayerTest extends TestCase {
assertThat(audioRenderer.isEnded).isTrue();
}
@Test
public void testRepreparationGivesFreshSourceInfo() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
Object firstSourceManifest = new Object();
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
Builder.VIDEO_FORMAT);
MediaSource firstSource =
new FakeMediaSource(timeline, firstSourceManifest, Builder.VIDEO_FORMAT);
final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
MediaSource secondSource =
@ -218,10 +228,8 @@ public final class ExoPlayerTest extends TestCase {
ExoPlayer player, boolean isTopLevelSource, Listener listener) {
super.prepareSource(player, isTopLevelSource, listener);
// We've queued a source info refresh on the playback thread's event queue. Allow the
// test
// thread to prepare the player with the third source, and block this thread (the
// playback
// thread) until the test thread's call to prepare() has returned.
// test thread to prepare the player with the third source, and block this thread (the
// playback thread) until the test thread's call to prepare() has returned.
queuedSourceInfoCountDownLatch.countDown();
try {
completePreparationCountDownLatch.await();
@ -231,17 +239,19 @@ public final class ExoPlayerTest extends TestCase {
}
};
Object thirdSourceManifest = new Object();
MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest,
Builder.VIDEO_FORMAT);
MediaSource thirdSource =
new FakeMediaSource(timeline, thirdSourceManifest, Builder.VIDEO_FORMAT);
// Prepare the player with a source with the first manifest and a non-empty timeline. Prepare
// the player again with a source and a new manifest, which will never be exposed. Allow the
// test thread to prepare the player with a third source, and block the playback thread until
// the test thread's call to prepare() has returned.
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation")
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRepreparation")
.waitForTimelineChanged(timeline)
.prepareSource(secondSource)
.executeRunnable(new Runnable() {
.executeRunnable(
new Runnable() {
@Override
public void run() {
try {
@ -252,7 +262,8 @@ public final class ExoPlayerTest extends TestCase {
}
})
.prepareSource(thirdSource)
.executeRunnable(new Runnable() {
.executeRunnable(
new Runnable() {
@Override
public void run() {
completePreparationCountDownLatch.countDown();
@ -273,12 +284,15 @@ public final class ExoPlayerTest extends TestCase {
// info refresh from the second source was suppressed as we re-prepared with the third source.
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest);
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
assertThat(renderer.isEnded).isTrue();
}
@Test
public void testRepeatModeChanges() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
@ -301,9 +315,14 @@ public final class ExoPlayerTest extends TestCase {
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
@ -320,6 +339,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(renderer.isEnded).isTrue();
}
@Test
public void testShuffleModeEnabledChanges() throws Exception {
Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource[] fakeMediaSources = {
@ -327,8 +347,8 @@ public final class ExoPlayerTest extends TestCase {
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)
};
ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false,
new FakeShuffleOrder(3), fakeMediaSources);
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(false, new FakeShuffleOrder(3), fakeMediaSources);
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testShuffleModeEnabled")
@ -342,9 +362,14 @@ public final class ExoPlayerTest extends TestCase {
.setRepeatMode(Player.REPEAT_MODE_OFF)
.play()
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setRenderers(renderer)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
@ -355,6 +380,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(renderer.isEnded).isTrue();
}
@Test
public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule =
@ -374,6 +400,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(renderer.isEnded).isTrue();
}
@Test
public void testSeekProcessedCallback() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
ActionSchedule actionSchedule =
@ -387,7 +414,7 @@ public final class ExoPlayerTest extends TestCase {
.seek(2)
.seek(10)
// Wait until media source prepared and re-seek to same position. Expect a seek
// processed while still being in Player.STATE_READY.
// processed while still being in STATE_READY.
.waitForPlaybackState(Player.STATE_READY)
.seek(10)
// Start playback and wait until playback reaches second window.
@ -438,6 +465,7 @@ public final class ExoPlayerTest extends TestCase {
.inOrder();
}
@Test
public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception {
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition")
@ -468,21 +496,29 @@ public final class ExoPlayerTest extends TestCase {
assertThat(onSeekProcessedCalled[0]).isTrue();
}
@Test
public void testSeekDiscontinuity() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity")
.seek(10).build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setTimeline(timeline)
.setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS);
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSeekDiscontinuity").seek(10).build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
}
@Test
public void testSeekDiscontinuityWithAdjustment() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
@Override
protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id,
TrackGroupArray trackGroupArray, Allocator allocator) {
protected FakeMediaPeriod createFakeMediaPeriod(
MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) {
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
mediaPeriod.setSeekToUsOffset(10);
return mediaPeriod;
@ -495,45 +531,63 @@ public final class ExoPlayerTest extends TestCase {
.seek(10)
.play()
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource)
.setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK,
Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
@Test
public void testInternalDiscontinuityAtNewPosition() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
@Override
protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id,
TrackGroupArray trackGroupArray, Allocator allocator) {
protected FakeMediaPeriod createFakeMediaPeriod(
MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) {
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
mediaPeriod.setDiscontinuityPositionUs(10);
return mediaPeriod;
}
};
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL);
}
@Test
public void testInternalDiscontinuityAtInitialPosition() throws Exception {
FakeTimeline timeline = new FakeTimeline(1);
FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
FakeMediaSource mediaSource =
new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) {
@Override
protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id,
TrackGroupArray trackGroupArray, Allocator allocator) {
protected FakeMediaPeriod createFakeMediaPeriod(
MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) {
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
mediaPeriod.setDiscontinuityPositionUs(0);
return mediaPeriod;
}
};
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(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 testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource mediaSource =
@ -564,6 +618,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(numSelectionsEnabled).isEqualTo(2);
}
@Test
public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource mediaSource =
@ -594,6 +649,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(numSelectionsEnabled).isEqualTo(4);
}
@Test
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade()
throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
@ -640,6 +696,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(numSelectionsEnabled).isEqualTo(3);
}
@Test
public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed()
throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
@ -686,6 +743,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(numSelectionsEnabled).isEqualTo(3);
}
@Test
public void testDynamicTimelineChangeReason() throws Exception {
Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000));
final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000));
@ -704,27 +762,36 @@ public final class ExoPlayerTest extends TestCase {
.waitForTimelineChanged(timeline2)
.play()
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesEqual(timeline1, timeline2);
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_DYNAMIC);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_DYNAMIC);
}
@Test
public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception {
Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(/* isSeekable= */ true,
/* isDynamic= */ false, /* durationUs= */ 100000));
ConcatenatingMediaSource firstMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false,
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, null, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)
);
ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false,
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT));
ConcatenatingMediaSource secondMediaSource =
new ConcatenatingMediaSource(
/* isAtomic= */ false,
new FakeShuffleOrder(/* length= */ 2),
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)
);
new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRepreparationWithShuffle")
// Wait for first preparation and enable shuffling. Plays period 0.
@ -736,12 +803,17 @@ public final class ExoPlayerTest extends TestCase {
.prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false)
.play()
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
.setMediaSource(firstMediaSource).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setMediaSource(firstMediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0);
}
@Test
public void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception {
// Test that no exception is thrown when playback parameters are updated between creating a
// period and preparation of the period completing.
@ -763,7 +835,8 @@ public final class ExoPlayerTest extends TestCase {
new ActionSchedule.Builder("testSetPlaybackParametersBeforePreparationCompletesSucceeds")
.waitForPlaybackState(Player.STATE_BUFFERING)
// Block until createPeriod has been called on the fake media source.
.executeRunnable(new Runnable() {
.executeRunnable(
new Runnable() {
@Override
public void run() {
try {
@ -776,17 +849,23 @@ public final class ExoPlayerTest extends TestCase {
// Set playback parameters (while the fake media period is not yet prepared).
.setPlaybackParameters(new PlaybackParameters(2f, 2f))
// Complete preparation of the fake media period.
.executeRunnable(new Runnable() {
.executeRunnable(
new Runnable() {
@Override
public void run() {
fakeMediaPeriodHolder[0].setPreparationComplete();
}
})
.build();
new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule)
.build().start().blockUntilEnded(TIMEOUT_MS);
new ExoPlayerTestRunner.Builder()
.setMediaSource(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void testStopDoesNotResetPosition() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final long[] positionHolder = new long[1];
@ -818,6 +897,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(positionHolder[0]).isAtLeast(50L);
}
@Test
public void testStopWithoutResetDoesNotResetPosition() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final long[] positionHolder = new long[1];
@ -849,6 +929,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(positionHolder[0]).isAtLeast(50L);
}
@Test
public void testStopWithResetDoesResetPosition() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final long[] positionHolder = new long[1];
@ -875,21 +956,24 @@ public final class ExoPlayerTest extends TestCase {
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY);
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_RESET);
testRunner.assertNoPositionDiscontinuities();
assertThat(positionHolder[0]).isEqualTo(0);
}
@Test
public void testStopWithoutResetReleasesMediaSource() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource")
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testStopReleasesMediaSource")
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ false)
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
@ -899,15 +983,18 @@ public final class ExoPlayerTest extends TestCase {
testRunner.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void testStopWithResetReleasesMediaSource() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource")
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testStopReleasesMediaSource")
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true)
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build()
@ -917,16 +1004,19 @@ public final class ExoPlayerTest extends TestCase {
testRunner.blockUntilEnded(TIMEOUT_MS);
}
@Test
public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop")
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testRepreparationAfterStop")
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true)
.waitForPlaybackState(Player.STATE_IDLE)
.prepareSource(secondSource)
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.setExpectedPlayerEndedCount(2)
@ -934,16 +1024,20 @@ public final class ExoPlayerTest extends TestCase {
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertNoPositionDiscontinuities();
}
@Test
public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2);
MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT);
ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset")
ActionSchedule actionSchedule =
new ActionSchedule.Builder("testSeekAfterStopWithReset")
.waitForPlaybackState(Player.STATE_READY)
.stop(/* reset= */ true)
.waitForPlaybackState(Player.STATE_IDLE)
@ -951,7 +1045,8 @@ public final class ExoPlayerTest extends TestCase {
.seek(/* windowIndex= */ 1, /* positionMs= */ 0)
.prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true)
.build();
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder()
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.setExpectedPlayerEndedCount(2)
@ -959,12 +1054,15 @@ public final class ExoPlayerTest extends TestCase {
.start()
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline);
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PREPARED,
Player.TIMELINE_CHANGE_REASON_RESET,
Player.TIMELINE_CHANGE_REASON_PREPARED);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
testRunner.assertPlayedPeriodIndices(0, 1);
}
@Test
public void testStopDuringPreparationOverwritesPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
ActionSchedule actionSchedule =
@ -987,6 +1085,7 @@ public final class ExoPlayerTest extends TestCase {
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
}
@Test
public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception {
// Combining additional stop and seek after initial stop in one test to get the seek processed
// callback which ensures that all operations have been processed by the player.
@ -1012,6 +1111,7 @@ public final class ExoPlayerTest extends TestCase {
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
}
@Test
public void testReprepareAfterPlaybackError() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
ActionSchedule actionSchedule =
@ -1043,6 +1143,7 @@ public final class ExoPlayerTest extends TestCase {
Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED);
}
@Test
public void testSeekAndReprepareAfterPlaybackError() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final long[] positionHolder = new long[2];
@ -1098,6 +1199,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(positionHolder[1]).isEqualTo(50);
}
@Test
public void testPlaybackErrorDuringSourceInfoRefreshStillUpdatesTimeline() throws Exception {
final Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
final FakeMediaSource mediaSource =
@ -1133,6 +1235,7 @@ public final class ExoPlayerTest extends TestCase {
testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED);
}
@Test
public void testSendMessagesDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1152,6 +1255,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1171,6 +1275,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testMultipleSendMessages() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget();
@ -1194,6 +1299,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target80.positionMs).isAtLeast(target50.positionMs);
}
@Test
public void testMultipleSendMessagesAtSameTime() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget();
@ -1216,6 +1322,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target2.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesMultiPeriodResolution() throws Exception {
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0));
@ -1236,6 +1343,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesAtStartAndEndOfPeriod() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget();
@ -1279,6 +1387,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(targetEndLastPeriod.positionMs).isAtLeast(duration2Ms);
}
@Test
public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1297,6 +1406,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1316,6 +1426,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1336,6 +1447,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);
}
@Test
public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1356,6 +1468,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET);
}
@Test
public void testSendMessagesRepeatDoesNotRepost() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1379,6 +1492,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1407,6 +1521,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesMoveCurrentWindowIndex() throws Exception {
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));
@ -1441,6 +1556,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.windowIndex).isEqualTo(1);
}
@Test
public void testSendMessagesMultiWindowDuringPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1461,6 +1577,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesMultiWindowAfterPreparation() throws Exception {
Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget();
@ -1481,6 +1598,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.positionMs >= 50).isTrue();
}
@Test
public void testSendMessagesMoveWindowIndex() throws Exception {
Timeline timeline =
new FakeTimeline(
@ -1518,6 +1636,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target.windowIndex).isEqualTo(0);
}
@Test
public void testSendMessagesNonLinearPeriodOrder() throws Exception {
Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
MediaSource[] fakeMediaSources = {
@ -1552,6 +1671,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(target3.windowIndex).isEqualTo(2);
}
@Test
public void testSetAndSwitchSurface() throws Exception {
final List<Integer> rendererMessages = new ArrayList<>();
Renderer videoRenderer =
@ -1574,6 +1694,7 @@ public final class ExoPlayerTest extends TestCase {
assertThat(Collections.frequency(rendererMessages, C.MSG_SET_SURFACE)).isEqualTo(2);
}
@Test
public void testSwitchSurfaceOnEndedState() throws Exception {
ActionSchedule.Builder scheduleBuilder =
new ActionSchedule.Builder("testSwitchSurfaceOnEndedState")
@ -1591,8 +1712,8 @@ public final class ExoPlayerTest extends TestCase {
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
final Surface surface1 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false);
final Surface surface2 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false);
final Surface surface1 = new Surface(null);
final Surface surface2 = new Surface(null);
return builder
.executeRunnable(
new PlayerRunnable() {

View File

@ -0,0 +1,203 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.callInstanceMethod;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowMessageQueue;
/** Collection of shadow classes used to run tests with Robolectric which require Loopers. */
public final class RobolectricUtil {
private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0);
private RobolectricUtil() {}
/**
* A custom implementation of Robolectric's ShadowLooper which runs all scheduled messages in the
* loop method of the looper. Also ensures to correctly emulate the message order of the real
* message loop and to avoid blocking caused by Robolectric's default implementation.
*
* <p>Only works in conjunction with {@link CustomMessageQueue}. Note that the test's {@code
* SystemClock} is not advanced automatically.
*/
@Implements(Looper.class)
public static final class CustomLooper extends ShadowLooper {
private final PriorityBlockingQueue<PendingMessage> pendingMessages;
private final CopyOnWriteArraySet<RemovedMessage> removedMessages;
public CustomLooper() {
pendingMessages = new PriorityBlockingQueue<>();
removedMessages = new CopyOnWriteArraySet<>();
}
@Implementation
public static void loop() {
ShadowLooper looper = shadowOf(Looper.myLooper());
if (looper instanceof CustomLooper) {
((CustomLooper) looper).doLoop();
}
}
@Implementation
@Override
public void quitUnchecked() {
super.quitUnchecked();
// Insert message at the front of the queue to quit loop as soon as possible.
addPendingMessage(/* message= */ null, /* when= */ Long.MIN_VALUE);
}
private void addPendingMessage(@Nullable Message message, long when) {
pendingMessages.put(new PendingMessage(message, when));
}
private void removeMessages(Handler handler, int what, Object object) {
RemovedMessage newRemovedMessage = new RemovedMessage(handler, what, object);
removedMessages.add(newRemovedMessage);
for (RemovedMessage removedMessage : removedMessages) {
if (removedMessage != newRemovedMessage
&& removedMessage.handler == handler
&& removedMessage.what == what
&& removedMessage.object == object) {
removedMessages.remove(removedMessage);
}
}
}
private void doLoop() {
try {
while (true) {
PendingMessage pendingMessage = pendingMessages.take();
if (pendingMessage.message == null) {
// Null message is signal to end message loop.
return;
}
// Call through to real {@code Message.markInUse()} and {@code Message.recycle()} to
// ensure message recycling works. This is also done in Robolectric's own implementation
// of the message queue.
callInstanceMethod(pendingMessage.message, "markInUse");
Handler target = pendingMessage.message.getTarget();
if (target != null) {
boolean isRemoved = false;
for (RemovedMessage removedMessage : removedMessages) {
if (removedMessage.handler == target
&& removedMessage.what == pendingMessage.message.what
&& (removedMessage.object == null
|| removedMessage.object == pendingMessage.message.obj)
&& pendingMessage.sequenceNumber < removedMessage.sequenceNumber) {
isRemoved = true;
}
}
if (!isRemoved) {
target.dispatchMessage(pendingMessage.message);
}
}
if (Util.SDK_INT >= 21) {
callInstanceMethod(pendingMessage.message, "recycleUnchecked");
} else {
callInstanceMethod(pendingMessage.message, "recycle");
}
}
} catch (InterruptedException e) {
// Ignore.
}
}
}
/**
* Custom implementation of Robolectric's ShadowMessageQueue which is needed to let {@link
* CustomLooper} work as intended.
*/
@Implements(MessageQueue.class)
public static final class CustomMessageQueue extends ShadowMessageQueue {
private final Thread looperThread;
public CustomMessageQueue() {
looperThread = Thread.currentThread();
}
@Implementation
@Override
public boolean enqueueMessage(Message msg, long when) {
ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread));
if (looper instanceof CustomLooper) {
((CustomLooper) looper).addPendingMessage(msg, when);
}
return true;
}
@Implementation
public void removeMessages(Handler handler, int what, Object object) {
ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread));
if (looper instanceof CustomLooper) {
((CustomLooper) looper).removeMessages(handler, what, object);
}
}
}
private static final class PendingMessage implements Comparable<PendingMessage> {
public final @Nullable Message message;
public final long when;
public final long sequenceNumber;
public PendingMessage(@Nullable Message message, long when) {
this.message = message;
this.when = when;
sequenceNumber = sequenceNumberGenerator.getAndIncrement();
}
@Override
public int compareTo(@NonNull PendingMessage other) {
int res = Long.compare(this.when, other.when);
if (res == 0 && this != other) {
res = Long.compare(this.sequenceNumber, other.sequenceNumber);
}
return res;
}
}
private static final class RemovedMessage {
public final Handler handler;
public final int what;
public final Object object;
public final long sequenceNumber;
public RemovedMessage(Handler handler, int what, Object object) {
this.handler = handler;
this.what = what;
this.object = object;
this.sequenceNumber = sequenceNumberGenerator.get();
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.testutil;
import com.google.android.exoplayer2.util.HandlerWrapper;
/**
* {@link FakeClock} extension which automatically advances time whenever an empty message is
* enqueued at a future time. The clock time is advanced to the time of the message. Only the first
* Handler sending messages at a future time will be allowed to advance time to ensure there is only
* one "time master". This should usually be the Handler of the internal playback loop.
*/
public final class AutoAdvancingFakeClock extends FakeClock {
private HandlerWrapper autoAdvancingHandler;
public AutoAdvancingFakeClock() {
super(/* initialTimeMs= */ 0);
}
@Override
protected synchronized boolean addHandlerMessageAtTime(
HandlerWrapper handler, int message, long timeMs) {
boolean result = super.addHandlerMessageAtTime(handler, message, timeMs);
if (autoAdvancingHandler == null || autoAdvancingHandler == handler) {
autoAdvancingHandler = handler;
long currentTimeMs = elapsedRealtime();
if (currentTimeMs < timeMs) {
advanceTime(timeMs - currentTimeMs);
}
}
return result;
}
}

View File

@ -203,8 +203,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
}
/**
* Sets the {@link Clock} to be used by the test runner. The default value is {@link
* Clock#DEFAULT}.
* Sets the {@link Clock} to be used by the test runner. The default value is a {@link
* AutoAdvancingFakeClock}.
*
* @param clock A {@link Clock} to be used by the test runner.
* @return This builder.
@ -307,7 +307,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
loadControl = new DefaultLoadControl();
}
if (clock == null) {
clock = Clock.DEFAULT;
clock = new AutoAdvancingFakeClock();
}
if (mediaSource == null) {
if (timeline == null) {