Update TestPlayerRunHelper
to fail on non-fatal errors by default
Also introduce a fluent API that allows callers to ignore non-fatal errors (while avoiding adding boolean overloads for every method). **Most** tests want to fail on non-fatal errors (since they likely indicate user-visible issues like codec errors etc), only tests explicitly testing fallback in error scenarios should want to ignore them. Before this change there were a few `playUntilXXX` methods. These can now all be triggered via `play(player).untilXXX`, which means effectively every 'until' condition is available in a 'play until' variant that calls `play` just before waiting for the condition. PiperOrigin-RevId: 608988234
This commit is contained in:
parent
2e9cc2784f
commit
bb5c688543
@ -50,6 +50,11 @@
|
|||||||
* Cast Extension:
|
* Cast Extension:
|
||||||
* Test Utilities:
|
* Test Utilities:
|
||||||
* Implement `onInit()` and `onRelease()` in `FakeRenderer`.
|
* Implement `onInit()` and `onRelease()` in `FakeRenderer`.
|
||||||
|
* Change `TestPlayerRunHelper.runUntil/playUntil` methods to fail on
|
||||||
|
non-fatal errors (e.g. those reported to
|
||||||
|
`AnalyticsListener.onVideoCodecError`). Use the new
|
||||||
|
`TestPlayerRunHelper.run(player).ignoringNonFatalErrors().untilXXX()`
|
||||||
|
method chain to disable this behavior.
|
||||||
* Remove deprecated symbols:
|
* Remove deprecated symbols:
|
||||||
* Demo app:
|
* Demo app:
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ import static androidx.media3.common.util.Assertions.checkState;
|
|||||||
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
|
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
|
||||||
|
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
@ -29,25 +31,451 @@ import androidx.media3.common.util.UnstableApi;
|
|||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
|
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
||||||
|
import androidx.media3.exoplayer.source.LoadEventInfo;
|
||||||
|
import androidx.media3.exoplayer.source.MediaLoadData;
|
||||||
import androidx.media3.test.utils.ThreadTestUtil;
|
import androidx.media3.test.utils.ThreadTestUtil;
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper methods to block the calling thread until the provided {@link ExoPlayer} instance reaches
|
* Helper methods to block the calling thread until the provided {@link ExoPlayer} instance reaches
|
||||||
* a particular state.
|
* a particular state.
|
||||||
|
*
|
||||||
|
* <p>This class has two usage modes:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Fluent method chaining, e.g. {@code
|
||||||
|
* run(player).ignoringNonFatalErrors().untilState(STATE_ENDED)}.
|
||||||
|
* <li>Single method call, e.g. {@code runUntilPlaybackState(player, STATE_ENDED)}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer the fluent method chaining, and new functionality will only be added
|
||||||
|
* to this form. The older single methods will be kept for backwards compatibility.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class TestPlayerRunHelper {
|
public final class TestPlayerRunHelper {
|
||||||
|
|
||||||
private TestPlayerRunHelper() {}
|
private TestPlayerRunHelper() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link Player#getPlaybackState()} matches the
|
* Intermediate type that allows callers to run the main {@link Looper} until certain conditions
|
||||||
* expected state or a playback error occurs.
|
* are met.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If an error occurs while a {@code untilXXX(...)} method is waiting for the condition to
|
||||||
|
* become true, most methods will throw that error (exceptions to this are documented on specific
|
||||||
|
* methods below). Use {@link #ignoringNonFatalErrors()} to ignore non-fatal errors and only fail
|
||||||
|
* on {@linkplain Player.Listener#getPlayerError() fatal playback errors}.
|
||||||
|
*
|
||||||
|
* <p>Instances of this class should only be used for a single {@code untilXXX()} invocation and
|
||||||
|
* not be re-used.
|
||||||
|
*/
|
||||||
|
public static class PlayerRunResult {
|
||||||
|
private final Player player;
|
||||||
|
private final boolean throwNonFatalErrors;
|
||||||
|
|
||||||
|
protected final boolean playBeforeWaiting;
|
||||||
|
|
||||||
|
protected boolean hasBeenUsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance.
|
||||||
|
*
|
||||||
|
* @param player The player to interact with.
|
||||||
|
* @param playBeforeWaiting Whether to call {@link Player#play()} before waiting for the chosen
|
||||||
|
* condition.
|
||||||
|
* @param throwNonFatalErrors Whether to throw non-fatal errors passed to {@link
|
||||||
|
* AnalyticsListener}.
|
||||||
|
*/
|
||||||
|
// This constructor is deliberately private to prevent subclassing outside TestPlayerRunHelper.
|
||||||
|
private PlayerRunResult(Player player, boolean playBeforeWaiting, boolean throwNonFatalErrors) {
|
||||||
|
verifyMainTestThread(player);
|
||||||
|
if (player instanceof ExoPlayer) {
|
||||||
|
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
||||||
|
}
|
||||||
|
this.player = player;
|
||||||
|
this.playBeforeWaiting = playBeforeWaiting;
|
||||||
|
this.throwNonFatalErrors = throwNonFatalErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player#getPlaybackState()} matches the
|
||||||
|
* expected state or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@linkplain RobolectricUtil#DEFAULT_TIMEOUT_MS default
|
||||||
|
* timeout} is exceeded.
|
||||||
|
*/
|
||||||
|
public final void untilState(@Player.State int expectedState) throws Exception {
|
||||||
|
runUntil(() -> player.getPlaybackState() == expectedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player#getPlayWhenReady()} matches the
|
||||||
|
* expected value or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@linkplain RobolectricUtil#DEFAULT_TIMEOUT_MS default
|
||||||
|
* timeout} is exceeded.
|
||||||
|
*/
|
||||||
|
public final void untilPlayWhenReadyIs(boolean expectedPlayWhenReady) throws Exception {
|
||||||
|
runUntil(() -> player.getPlayWhenReady() == expectedPlayWhenReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player#isLoading()} matches the expected
|
||||||
|
* value or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@linkplain RobolectricUtil#DEFAULT_TIMEOUT_MS default
|
||||||
|
* timeout} is exceeded.
|
||||||
|
*/
|
||||||
|
public final void untilLoadingIs(boolean expectedIsLoading) throws Exception {
|
||||||
|
runUntil(() -> player.isLoading() == expectedIsLoading);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until a timeline change or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public final Timeline untilTimelineChanges() throws Exception {
|
||||||
|
AtomicReference<@NullableType Timeline> receivedTimeline = new AtomicReference<>();
|
||||||
|
Player.Listener listener =
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
|
receivedTimeline.set(timeline);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
player.addListener(listener);
|
||||||
|
try {
|
||||||
|
runUntil(() -> receivedTimeline.get() != null);
|
||||||
|
return checkNotNull(receivedTimeline.get());
|
||||||
|
} finally {
|
||||||
|
player.removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
|
||||||
|
* expected timeline or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public final void untilTimelineChangesTo(Timeline expectedTimeline) throws Exception {
|
||||||
|
runUntil(() -> expectedTimeline.equals(player.getCurrentTimeline()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link
|
||||||
|
* Player.Listener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)} is
|
||||||
|
* called with the specified {@link Player.DiscontinuityReason} or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public final void untilPositionDiscontinuityWithReason(
|
||||||
|
@Player.DiscontinuityReason int expectedReason) throws Exception {
|
||||||
|
AtomicBoolean receivedExpectedDiscontinuityReason = new AtomicBoolean(false);
|
||||||
|
Player.Listener listener =
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(
|
||||||
|
Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
||||||
|
if (reason == expectedReason) {
|
||||||
|
receivedExpectedDiscontinuityReason.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
player.addListener(listener);
|
||||||
|
try {
|
||||||
|
runUntil(receivedExpectedDiscontinuityReason::get);
|
||||||
|
} finally {
|
||||||
|
player.removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until a player error occurs.
|
||||||
|
*
|
||||||
|
* <p>Non-fatal errors are always ignored.
|
||||||
|
*
|
||||||
|
* @return The raised {@link PlaybackException}.
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public PlaybackException untilPlayerError() throws TimeoutException {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
runMainLooperUntil(() -> player.getPlayerError() != null);
|
||||||
|
return checkNotNull(player.getPlayerError());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player.Listener#onRenderedFirstFrame} is
|
||||||
|
* called or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public void untilFirstFrameIsRendered() throws Exception {
|
||||||
|
AtomicBoolean receivedFirstFrameRenderedCallback = new AtomicBoolean(false);
|
||||||
|
Player.Listener listener =
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onRenderedFirstFrame() {
|
||||||
|
receivedFirstFrameRenderedCallback.set(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
player.addListener(listener);
|
||||||
|
try {
|
||||||
|
runUntil(receivedFirstFrameRenderedCallback::get);
|
||||||
|
} finally {
|
||||||
|
player.removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance where the {@code untilXXX(...)} methods ignore non-fatal errors.
|
||||||
|
*
|
||||||
|
* <p>A fatal error is defined as an error that is passed to {@link
|
||||||
|
* Player.Listener#onPlayerError(PlaybackException)} and results in the player transitioning to
|
||||||
|
* {@link Player#STATE_IDLE}. A non-fatal error is defined as an error that is passed to any
|
||||||
|
* other callback (e.g. {@link AnalyticsListener#onLoadError}).
|
||||||
|
*/
|
||||||
|
public PlayerRunResult ignoringNonFatalErrors() {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
return new PlayerRunResult(player, playBeforeWaiting, /* throwNonFatalErrors= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs the main {@link Looper} until {@code predicate} returns true or an error occurs. */
|
||||||
|
protected final void runUntil(Supplier<Boolean> predicate) throws Exception {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
ErrorListener errorListener = new ErrorListener(throwNonFatalErrors);
|
||||||
|
if (player instanceof ExoPlayer) {
|
||||||
|
ExoPlayer exoplayer = (ExoPlayer) player;
|
||||||
|
exoplayer.addAnalyticsListener(errorListener);
|
||||||
|
}
|
||||||
|
player.addListener(errorListener);
|
||||||
|
if (playBeforeWaiting) {
|
||||||
|
player.play();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
runMainLooperUntil(() -> predicate.get() || errorListener.hasFatalError());
|
||||||
|
} finally {
|
||||||
|
player.removeListener(errorListener);
|
||||||
|
if (player instanceof ExoPlayer) {
|
||||||
|
((ExoPlayer) player).removeAnalyticsListener(errorListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errorListener.maybeThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link ExoPlayer} specific subclass of {@link PlayerRunResult}, giving access to conditions
|
||||||
|
* that only make sense for the {@link ExoPlayer} interface.
|
||||||
|
*/
|
||||||
|
public static final class ExoPlayerRunResult extends PlayerRunResult {
|
||||||
|
|
||||||
|
private final ExoPlayer player;
|
||||||
|
|
||||||
|
private ExoPlayerRunResult(
|
||||||
|
ExoPlayer player, boolean playBeforeWaiting, boolean throwNonFatalErrors) {
|
||||||
|
super(player, playBeforeWaiting, throwNonFatalErrors);
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExoPlaybackException untilPlayerError() throws TimeoutException {
|
||||||
|
return (ExoPlaybackException) super.untilPlayerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link ExoPlayer#isSleepingForOffload()} matches
|
||||||
|
* the expected value, or an error occurs.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public void untilSleepingForOffloadBecomes(boolean expectedSleepingForOffload)
|
||||||
|
throws Exception {
|
||||||
|
AtomicBoolean receivedExpectedValue = new AtomicBoolean(false);
|
||||||
|
ExoPlayer.AudioOffloadListener listener =
|
||||||
|
new ExoPlayer.AudioOffloadListener() {
|
||||||
|
@Override
|
||||||
|
public void onSleepingForOffloadChanged(boolean sleepingForOffload) {
|
||||||
|
if (sleepingForOffload == expectedSleepingForOffload) {
|
||||||
|
receivedExpectedValue.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
player.addAudioOffloadListener(listener);
|
||||||
|
try {
|
||||||
|
runUntil(receivedExpectedValue::get);
|
||||||
|
} finally {
|
||||||
|
player.removeAudioOffloadListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until playback reaches the specified position or an
|
||||||
|
* error occurs.
|
||||||
|
*
|
||||||
|
* <p>The playback thread is automatically blocked from making further progress after reaching
|
||||||
|
* this position and will only be unblocked by other {@code run()/play().untilXXX(...)} method
|
||||||
|
* chains, custom {@link RobolectricUtil#runMainLooperUntil} conditions, or an explicit {@link
|
||||||
|
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public void untilPosition(int mediaItemIndex, long positionMs) throws Exception {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
Looper applicationLooper = Util.getCurrentOrMainLooper();
|
||||||
|
AtomicBoolean messageHandled = new AtomicBoolean(false);
|
||||||
|
player
|
||||||
|
.createMessage(
|
||||||
|
(messageType, payload) -> {
|
||||||
|
// Block playback thread until the main app thread is able to trigger further
|
||||||
|
// actions.
|
||||||
|
ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();
|
||||||
|
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
||||||
|
blockPlaybackThreadCondition, applicationLooper);
|
||||||
|
player
|
||||||
|
.getClock()
|
||||||
|
.createHandler(applicationLooper, /* callback= */ null)
|
||||||
|
.post(() -> messageHandled.set(true));
|
||||||
|
try {
|
||||||
|
player.getClock().onThreadBlocked();
|
||||||
|
blockPlaybackThreadCondition.block();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Ignore.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setPosition(mediaItemIndex, positionMs)
|
||||||
|
.send();
|
||||||
|
player.play();
|
||||||
|
runMainLooperUntil(() -> messageHandled.get() || player.getPlayerError() != null);
|
||||||
|
if (player.getPlayerError() != null) {
|
||||||
|
throw new IllegalStateException(player.getPlayerError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until playback reaches the specified media item or a
|
||||||
|
* playback error occurs.
|
||||||
|
*
|
||||||
|
* <p>The playback thread is automatically blocked from making further progress after reaching
|
||||||
|
* the media item and will only be unblocked by other {@code run()/play().untilXXX(...)} method
|
||||||
|
* chains, custom {@link RobolectricUtil#runMainLooperUntil} conditions, or an explicit {@link
|
||||||
|
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
||||||
|
*
|
||||||
|
* @param mediaItemIndex The index of the media item.
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public void untilStartOfMediaItem(int mediaItemIndex) throws Exception {
|
||||||
|
untilPosition(mediaItemIndex, /* positionMs= */ 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until the player completely handled all previously
|
||||||
|
* issued commands on the internal playback thread.
|
||||||
|
*
|
||||||
|
* <p>Both fatal and non-fatal errors are always ignored.
|
||||||
|
*
|
||||||
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
|
* exceeded.
|
||||||
|
*/
|
||||||
|
public void untilPendingCommandsAreFullyHandled() throws Exception {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
// Send message to player that will arrive after all other pending commands. Thus, the message
|
||||||
|
// execution on the app thread will also happen after all other pending command
|
||||||
|
// acknowledgements have arrived back on the app thread.
|
||||||
|
AtomicBoolean receivedMessageCallback = new AtomicBoolean(false);
|
||||||
|
player
|
||||||
|
.createMessage((type, data) -> receivedMessageCallback.set(true))
|
||||||
|
.setLooper(Util.getCurrentOrMainLooper())
|
||||||
|
.send();
|
||||||
|
runMainLooperUntil(receivedMessageCallback::get);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExoPlayerRunResult ignoringNonFatalErrors() {
|
||||||
|
checkState(!hasBeenUsed);
|
||||||
|
hasBeenUsed = true;
|
||||||
|
return new ExoPlayerRunResult(player, playBeforeWaiting, /* throwNonFatalErrors= */ false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for a fluent "wait for condition X" assertion.
|
||||||
|
*
|
||||||
|
* <p>Callers can use the returned {@link PlayerRunResult} to run the main {@link Looper} until
|
||||||
|
* certain conditions are met.
|
||||||
|
*/
|
||||||
|
public static PlayerRunResult run(Player player) {
|
||||||
|
return new PlayerRunResult(
|
||||||
|
player, /* playBeforeWaiting= */ false, /* throwNonFatalErrors= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for a fluent "wait for condition X" assertion.
|
||||||
|
*
|
||||||
|
* <p>Callers can use the returned {@link ExoPlayerRunResult} to run the main {@link Looper} until
|
||||||
|
* certain conditions are met.
|
||||||
|
*/
|
||||||
|
public static ExoPlayerRunResult run(ExoPlayer player) {
|
||||||
|
return new ExoPlayerRunResult(
|
||||||
|
player, /* playBeforeWaiting= */ false, /* throwNonFatalErrors= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for a fluent "start playback and wait for condition X" assertion.
|
||||||
|
*
|
||||||
|
* <p>Callers can use the returned {@link PlayerRunResult} to run the main {@link Looper} until
|
||||||
|
* certain conditions are met.
|
||||||
|
*
|
||||||
|
* <p>This is the same as {@link #run(Player)} but ensures {@link Player#play()} is called before
|
||||||
|
* waiting in subsequent {@code untilXXX(...)} methods.
|
||||||
|
*/
|
||||||
|
public static PlayerRunResult play(Player player) {
|
||||||
|
return new PlayerRunResult(
|
||||||
|
player, /* playBeforeWaiting= */ true, /* throwNonFatalErrors= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for a fluent "start playback and wait for condition X" assertion.
|
||||||
|
*
|
||||||
|
* <p>Callers can use the returned {@link ExoPlayerRunResult} to run the main {@link Looper} until
|
||||||
|
* certain conditions are met.
|
||||||
|
*
|
||||||
|
* <p>This is the same as {@link #run(ExoPlayer)} but ensures {@link ExoPlayer#play()} is called
|
||||||
|
* before waiting in subsequent {@code untilXXX(...)} methods.
|
||||||
|
*/
|
||||||
|
public static ExoPlayerRunResult play(ExoPlayer player) {
|
||||||
|
return new ExoPlayerRunResult(
|
||||||
|
player, /* playBeforeWaiting= */ true, /* throwNonFatalErrors= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs tasks of the main {@link Looper} until {@link Player#getPlaybackState()} matches the
|
||||||
|
* expected state or an error occurs.
|
||||||
|
*
|
||||||
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link PlayerRunResult#untilState(int)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedState The expected {@link Player.State}.
|
* @param expectedState The expected {@link Player.State}.
|
||||||
@ -56,22 +484,23 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilPlaybackState(Player player, @Player.State int expectedState)
|
public static void runUntilPlaybackState(Player player, @Player.State int expectedState)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
if (player instanceof ExoPlayer) {
|
run(player).untilState(expectedState);
|
||||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
}
|
throw e;
|
||||||
runMainLooperUntil(
|
} catch (Exception e) {
|
||||||
() -> player.getPlaybackState() == expectedState || player.getPlayerError() != null);
|
throw new IllegalStateException(e);
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link Player#getPlayWhenReady()} matches the
|
* Runs tasks of the main {@link Looper} until {@link Player#getPlayWhenReady()} matches the
|
||||||
* expected value or a playback error occurs.
|
* expected value or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilPlayWhenReadyIs(boolean)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedPlayWhenReady The expected value for {@link Player#getPlayWhenReady()}.
|
* @param expectedPlayWhenReady The expected value for {@link Player#getPlayWhenReady()}.
|
||||||
@ -80,23 +509,23 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)
|
public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
if (player instanceof ExoPlayer) {
|
run(player).untilPlayWhenReadyIs(expectedPlayWhenReady);
|
||||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
}
|
throw e;
|
||||||
runMainLooperUntil(
|
} catch (Exception e) {
|
||||||
() ->
|
throw new IllegalStateException(e);
|
||||||
player.getPlayWhenReady() == expectedPlayWhenReady || player.getPlayerError() != null);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link Player#isLoading()} matches the expected
|
* Runs tasks of the main {@link Looper} until {@link Player#isLoading()} matches the expected
|
||||||
* value or a playback error occurs.
|
* value or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilLoadingIs(boolean)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedIsLoading The expected value for {@link Player#isLoading()}.
|
* @param expectedIsLoading The expected value for {@link Player#isLoading()}.
|
||||||
@ -105,22 +534,23 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilIsLoading(Player player, boolean expectedIsLoading)
|
public static void runUntilIsLoading(Player player, boolean expectedIsLoading)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
if (player instanceof ExoPlayer) {
|
run(player).untilLoadingIs(expectedIsLoading);
|
||||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
}
|
throw e;
|
||||||
runMainLooperUntil(
|
} catch (Exception e) {
|
||||||
() -> player.isLoading() == expectedIsLoading || player.getPlayerError() != null);
|
throw new IllegalStateException(e);
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
|
* Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
|
||||||
* expected timeline or a playback error occurs.
|
* expected timeline or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilTimelineChangesTo(Timeline)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedTimeline The expected {@link Timeline}.
|
* @param expectedTimeline The expected {@link Timeline}.
|
||||||
@ -129,23 +559,22 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline)
|
public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
if (player instanceof ExoPlayer) {
|
run(player).untilTimelineChangesTo(expectedTimeline);
|
||||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
}
|
throw e;
|
||||||
runMainLooperUntil(
|
} catch (Exception e) {
|
||||||
() ->
|
throw new IllegalStateException(e);
|
||||||
expectedTimeline.equals(player.getCurrentTimeline())
|
|
||||||
|| player.getPlayerError() != null);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until a timeline change or a playback error occurs.
|
* Runs tasks of the main {@link Looper} until a timeline change or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilTimelineChanges()}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @return The new {@link Timeline}.
|
* @return The new {@link Timeline}.
|
||||||
@ -153,30 +582,24 @@ public class TestPlayerRunHelper {
|
|||||||
* exceeded.
|
* exceeded.
|
||||||
*/
|
*/
|
||||||
public static Timeline runUntilTimelineChanged(Player player) throws TimeoutException {
|
public static Timeline runUntilTimelineChanged(Player player) throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
AtomicReference<@NullableType Timeline> receivedTimeline = new AtomicReference<>();
|
return run(player).untilTimelineChanges();
|
||||||
Player.Listener listener =
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
new Player.Listener() {
|
throw e;
|
||||||
@Override
|
} catch (Exception e) {
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
throw new IllegalStateException(e);
|
||||||
receivedTimeline.set(timeline);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addListener(listener);
|
|
||||||
runMainLooperUntil(() -> receivedTimeline.get() != null || player.getPlayerError() != null);
|
|
||||||
player.removeListener(listener);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
return checkNotNull(receivedTimeline.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link
|
* Runs tasks of the main {@link Looper} until {@link
|
||||||
* Player.Listener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)} is
|
* Player.Listener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)} is
|
||||||
* called with the specified {@link Player.DiscontinuityReason} or a playback error occurs.
|
* called with the specified {@link Player.DiscontinuityReason} or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilPositionDiscontinuityWithReason(int)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedReason The expected {@link Player.DiscontinuityReason}.
|
* @param expectedReason The expected {@link Player.DiscontinuityReason}.
|
||||||
@ -185,51 +608,40 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilPositionDiscontinuity(
|
public static void runUntilPositionDiscontinuity(
|
||||||
Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
|
Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
if (player instanceof ExoPlayer) {
|
run(player).untilPositionDiscontinuityWithReason(expectedReason);
|
||||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
}
|
throw e;
|
||||||
AtomicBoolean receivedCallback = new AtomicBoolean(false);
|
} catch (Exception e) {
|
||||||
Player.Listener listener =
|
throw new IllegalStateException(e);
|
||||||
new Player.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onPositionDiscontinuity(
|
|
||||||
Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
|
|
||||||
if (reason == expectedReason) {
|
|
||||||
receivedCallback.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addListener(listener);
|
|
||||||
runMainLooperUntil(() -> receivedCallback.get() || player.getPlayerError() != null);
|
|
||||||
player.removeListener(listener);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until a player error occurs.
|
* Runs tasks of the main {@link Looper} until a player error occurs.
|
||||||
*
|
*
|
||||||
|
* <p>Non-fatal errors are ignored.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(ExoPlayer)} and {@link
|
||||||
|
* ExoPlayerRunResult#untilPlayerError()}.
|
||||||
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @return The raised {@link ExoPlaybackException}.
|
* @return The raised {@link ExoPlaybackException}.
|
||||||
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
* exceeded.
|
* exceeded.
|
||||||
*/
|
*/
|
||||||
public static ExoPlaybackException runUntilError(ExoPlayer player) throws TimeoutException {
|
public static ExoPlaybackException runUntilError(ExoPlayer player) throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
return run(player).untilPlayerError();
|
||||||
verifyPlaybackThreadIsAlive(player);
|
|
||||||
|
|
||||||
runMainLooperUntil(() -> player.getPlayerError() != null);
|
|
||||||
return checkNotNull(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until {@link
|
* Runs tasks of the main {@link Looper} until {@link ExoPlayer#isSleepingForOffload()} matches
|
||||||
* ExoPlayer.AudioOffloadListener#onSleepingForOffloadChanged(boolean)} is called or a playback
|
* the expected value, or an error occurs.
|
||||||
* error occurs.
|
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(ExoPlayer)} and {@link
|
||||||
|
* ExoPlayerRunResult#untilSleepingForOffloadBecomes(boolean)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param expectedSleepForOffload The expected sleep of offload state.
|
* @param expectedSleepForOffload The expected sleep of offload state.
|
||||||
@ -238,66 +650,51 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)
|
public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
verifyPlaybackThreadIsAlive(player);
|
run(player).untilSleepingForOffloadBecomes(expectedSleepForOffload);
|
||||||
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
AtomicBoolean receiverCallback = new AtomicBoolean(false);
|
throw e;
|
||||||
ExoPlayer.AudioOffloadListener listener =
|
} catch (Exception e) {
|
||||||
new ExoPlayer.AudioOffloadListener() {
|
throw new IllegalStateException(e);
|
||||||
@Override
|
|
||||||
public void onSleepingForOffloadChanged(boolean sleepingForOffload) {
|
|
||||||
if (sleepingForOffload == expectedSleepForOffload) {
|
|
||||||
receiverCallback.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addAudioOffloadListener(listener);
|
|
||||||
runMainLooperUntil(() -> receiverCallback.get() || player.getPlayerError() != null);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until the {@link Player.Listener#onRenderedFirstFrame}
|
* Runs tasks of the main {@link Looper} until {@link Player.Listener#onRenderedFirstFrame} is
|
||||||
* callback is called or a playback error occurs.
|
* called or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}..
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(Player)} and {@link
|
||||||
|
* PlayerRunResult#untilFirstFrameIsRendered()}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
* exceeded.
|
* exceeded.
|
||||||
*/
|
*/
|
||||||
public static void runUntilRenderedFirstFrame(ExoPlayer player) throws TimeoutException {
|
public static void runUntilRenderedFirstFrame(ExoPlayer player) throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
verifyPlaybackThreadIsAlive(player);
|
run(player).untilFirstFrameIsRendered();
|
||||||
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
AtomicBoolean receivedCallback = new AtomicBoolean(false);
|
throw e;
|
||||||
Player.Listener listener =
|
} catch (Exception e) {
|
||||||
new Player.Listener() {
|
throw new IllegalStateException(e);
|
||||||
@Override
|
|
||||||
public void onRenderedFirstFrame() {
|
|
||||||
receivedCallback.set(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addListener(listener);
|
|
||||||
runMainLooperUntil(() -> receivedCallback.get() || player.getPlayerError() != null);
|
|
||||||
player.removeListener(listener);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
|
* Calls {@link Player#play()} then runs tasks of the main {@link Looper} until the {@code player}
|
||||||
* reaches the specified position or a playback error occurs.
|
* reaches the specified position or an error occurs.
|
||||||
*
|
*
|
||||||
* <p>The playback thread is automatically blocked from making further progress after reaching
|
* <p>The playback thread is automatically blocked from making further progress after reaching
|
||||||
* this position and will only be unblocked by other {@code run/playUntil...} methods, custom
|
* this position and will only be unblocked by other {@code runUntil/playUntil...} methods, custom
|
||||||
* {@link RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
|
* {@link RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
|
||||||
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(ExoPlayer)} and {@link
|
||||||
|
* ExoPlayerRunResult#untilPosition(int, long)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param mediaItemIndex The index of the media item.
|
* @param mediaItemIndex The index of the media item.
|
||||||
@ -307,47 +704,28 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void playUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs)
|
public static void playUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
verifyPlaybackThreadIsAlive(player);
|
play(player).untilPosition(mediaItemIndex, positionMs);
|
||||||
Looper applicationLooper = Util.getCurrentOrMainLooper();
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
AtomicBoolean messageHandled = new AtomicBoolean(false);
|
throw e;
|
||||||
player
|
} catch (Exception e) {
|
||||||
.createMessage(
|
throw new IllegalStateException(e);
|
||||||
(messageType, payload) -> {
|
|
||||||
// Block playback thread until the main app thread is able to trigger further actions.
|
|
||||||
ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();
|
|
||||||
ThreadTestUtil.registerThreadIsBlockedUntilProgressOnLooper(
|
|
||||||
blockPlaybackThreadCondition, applicationLooper);
|
|
||||||
player
|
|
||||||
.getClock()
|
|
||||||
.createHandler(applicationLooper, /* callback= */ null)
|
|
||||||
.post(() -> messageHandled.set(true));
|
|
||||||
try {
|
|
||||||
player.getClock().onThreadBlocked();
|
|
||||||
blockPlaybackThreadCondition.block();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// Ignore.
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setPosition(mediaItemIndex, positionMs)
|
|
||||||
.send();
|
|
||||||
player.play();
|
|
||||||
runMainLooperUntil(() -> messageHandled.get() || player.getPlayerError() != null);
|
|
||||||
if (player.getPlayerError() != null) {
|
|
||||||
throw new IllegalStateException(player.getPlayerError());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
|
* Calls {@link Player#play()} then runs tasks of the main {@link Looper} until the {@code player}
|
||||||
* reaches the specified media item or a playback error occurs.
|
* reaches the specified media item or a playback error occurs.
|
||||||
*
|
*
|
||||||
* <p>The playback thread is automatically blocked from making further progress after reaching the
|
* <p>The playback thread is automatically blocked from making further progress after reaching the
|
||||||
* media item and will only be unblocked by other {@code run/playUntil...} methods, custom {@link
|
* media item and will only be unblocked by other {@code runUntil/playUntil...} methods, custom
|
||||||
* RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
|
* {@link RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
|
||||||
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
|
||||||
*
|
*
|
||||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
* <p>If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <p>New usages should prefer {@link #run(ExoPlayer)} and {@link
|
||||||
|
* ExoPlayerRunResult#untilStartOfMediaItem(int)}.
|
||||||
*
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @param mediaItemIndex The index of the media item.
|
* @param mediaItemIndex The index of the media item.
|
||||||
@ -356,31 +734,34 @@ public class TestPlayerRunHelper {
|
|||||||
*/
|
*/
|
||||||
public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)
|
public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
playUntilPosition(player, mediaItemIndex, /* positionMs= */ 0);
|
try {
|
||||||
|
play(player).untilStartOfMediaItem(mediaItemIndex);
|
||||||
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tasks of the main {@link Looper} until the player completely handled all previously issued
|
* Runs tasks of the main {@link Looper} until the player completely handled all previously issued
|
||||||
* commands on the internal playback thread.
|
* commands on the internal playback thread.
|
||||||
*
|
*
|
||||||
|
* <p>Both fatal and non-fatal errors are ignored.
|
||||||
|
*
|
||||||
* @param player The {@link Player}.
|
* @param player The {@link Player}.
|
||||||
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||||
* exceeded.
|
* exceeded.
|
||||||
*/
|
*/
|
||||||
public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player)
|
public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player)
|
||||||
throws TimeoutException {
|
throws TimeoutException {
|
||||||
verifyMainTestThread(player);
|
try {
|
||||||
verifyPlaybackThreadIsAlive(player);
|
run(player).untilPendingCommandsAreFullyHandled();
|
||||||
|
} catch (RuntimeException | TimeoutException e) {
|
||||||
// Send message to player that will arrive after all other pending commands. Thus, the message
|
throw e;
|
||||||
// execution on the app thread will also happen after all other pending command
|
} catch (Exception e) {
|
||||||
// acknowledgements have arrived back on the app thread.
|
throw new IllegalStateException(e);
|
||||||
AtomicBoolean receivedMessageCallback = new AtomicBoolean(false);
|
}
|
||||||
player
|
|
||||||
.createMessage((type, data) -> receivedMessageCallback.set(true))
|
|
||||||
.setLooper(Util.getCurrentOrMainLooper())
|
|
||||||
.send();
|
|
||||||
runMainLooperUntil(receivedMessageCallback::get);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void verifyMainTestThread(Player player) {
|
private static void verifyMainTestThread(Player player) {
|
||||||
@ -395,4 +776,92 @@ public class TestPlayerRunHelper {
|
|||||||
player.getPlaybackLooper().getThread().isAlive(),
|
player.getPlaybackLooper().getThread().isAlive(),
|
||||||
"Playback thread is not alive, has the player been released?");
|
"Playback thread is not alive, has the player been released?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Player.Listener} and {@link AnalyticsListener} that records errors.
|
||||||
|
*
|
||||||
|
* <p>All methods must be called on {@link Player#getApplicationLooper()}.
|
||||||
|
*/
|
||||||
|
private static final class ErrorListener implements AnalyticsListener, Player.Listener {
|
||||||
|
|
||||||
|
@Nullable private final List<Exception> nonFatalErrors;
|
||||||
|
private @MonotonicNonNull Exception fatalError;
|
||||||
|
|
||||||
|
public ErrorListener(boolean throwNonFatalErrors) {
|
||||||
|
if (throwNonFatalErrors) {
|
||||||
|
nonFatalErrors = new ArrayList<>();
|
||||||
|
} else {
|
||||||
|
nonFatalErrors = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFatalError() {
|
||||||
|
return fatalError != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void maybeThrow() throws Exception {
|
||||||
|
if (fatalError != null) {
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
if (nonFatalErrors != null && !nonFatalErrors.isEmpty()) {
|
||||||
|
IllegalStateException ise =
|
||||||
|
new IllegalStateException(
|
||||||
|
"Non-fatal errors detected. Attach an EventLogger and redirect logcat with"
|
||||||
|
+ " ShadowLog.stream to see full details.");
|
||||||
|
for (Exception nonFatalError : nonFatalErrors) {
|
||||||
|
ise.addSuppressed(nonFatalError);
|
||||||
|
}
|
||||||
|
throw ise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player.Listener impl
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
fatalError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyticsListener impl
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadError(
|
||||||
|
EventTime eventTime,
|
||||||
|
LoadEventInfo loadEventInfo,
|
||||||
|
MediaLoadData mediaLoadData,
|
||||||
|
IOException error,
|
||||||
|
boolean wasCanceled) {
|
||||||
|
if (nonFatalErrors != null) {
|
||||||
|
nonFatalErrors.add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioSinkError(EventTime eventTime, Exception audioSinkError) {
|
||||||
|
if (nonFatalErrors != null) {
|
||||||
|
nonFatalErrors.add(audioSinkError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioCodecError(EventTime eventTime, Exception audioCodecError) {
|
||||||
|
if (nonFatalErrors != null) {
|
||||||
|
nonFatalErrors.add(audioCodecError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoCodecError(EventTime eventTime, Exception videoCodecError) {
|
||||||
|
if (nonFatalErrors != null) {
|
||||||
|
nonFatalErrors.add(videoCodecError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
|
||||||
|
if (nonFatalErrors != null) {
|
||||||
|
nonFatalErrors.add(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user