diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e6a549acf8..6491948706 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -50,6 +50,11 @@ * Cast Extension: * Test Utilities: * 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: * Demo app: diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index 684917e17b..1cbd1f846b 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -21,6 +21,8 @@ import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import android.os.Looper; +import androidx.annotation.Nullable; +import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Timeline; 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.exoplayer.ExoPlaybackException; 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 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.atomic.AtomicBoolean; 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 * a particular state. + * + *
This class has two usage modes: + * + *
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 -public class TestPlayerRunHelper { +public final class TestPlayerRunHelper { private TestPlayerRunHelper() {} /** - * Runs tasks of the main {@link Looper} until {@link Player#getPlaybackState()} matches the - * expected state or a playback error occurs. + * Intermediate type that allows callers to run the main {@link Looper} until certain conditions + * are met. * - *
If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}. + *
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}. + * + *
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. + * + *
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. + * + *
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 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * Callers can use the returned {@link PlayerRunResult} to run the main {@link Looper} until
+ * certain conditions are met.
+ *
+ * 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.
+ *
+ * Callers can use the returned {@link ExoPlayerRunResult} to run the main {@link Looper} until
+ * certain conditions are met.
+ *
+ * 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.
+ *
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link PlayerRunResult#untilState(int)}.
*
* @param player The {@link Player}.
* @param expectedState The expected {@link Player.State}.
@@ -56,22 +484,23 @@ public class TestPlayerRunHelper {
*/
public static void runUntilPlaybackState(Player player, @Player.State int expectedState)
throws TimeoutException {
- verifyMainTestThread(player);
- if (player instanceof ExoPlayer) {
- verifyPlaybackThreadIsAlive((ExoPlayer) player);
- }
- runMainLooperUntil(
- () -> player.getPlaybackState() == expectedState || player.getPlayerError() != null);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
+ try {
+ run(player).untilState(expectedState);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
* 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.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilPlayWhenReadyIs(boolean)}.
*
* @param player The {@link Player}.
* @param expectedPlayWhenReady The expected value for {@link Player#getPlayWhenReady()}.
@@ -80,23 +509,23 @@ public class TestPlayerRunHelper {
*/
public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)
throws TimeoutException {
- verifyMainTestThread(player);
- if (player instanceof ExoPlayer) {
- verifyPlaybackThreadIsAlive((ExoPlayer) player);
- }
- runMainLooperUntil(
- () ->
- player.getPlayWhenReady() == expectedPlayWhenReady || player.getPlayerError() != null);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
+ try {
+ run(player).untilPlayWhenReadyIs(expectedPlayWhenReady);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
* 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.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilLoadingIs(boolean)}.
*
* @param player The {@link Player}.
* @param expectedIsLoading The expected value for {@link Player#isLoading()}.
@@ -105,22 +534,23 @@ public class TestPlayerRunHelper {
*/
public static void runUntilIsLoading(Player player, boolean expectedIsLoading)
throws TimeoutException {
- verifyMainTestThread(player);
- if (player instanceof ExoPlayer) {
- verifyPlaybackThreadIsAlive((ExoPlayer) player);
- }
- runMainLooperUntil(
- () -> player.isLoading() == expectedIsLoading || player.getPlayerError() != null);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
+ try {
+ run(player).untilLoadingIs(expectedIsLoading);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
* 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.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilTimelineChangesTo(Timeline)}.
*
* @param player The {@link Player}.
* @param expectedTimeline The expected {@link Timeline}.
@@ -129,23 +559,22 @@ public class TestPlayerRunHelper {
*/
public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline)
throws TimeoutException {
- verifyMainTestThread(player);
- if (player instanceof ExoPlayer) {
- verifyPlaybackThreadIsAlive((ExoPlayer) player);
- }
- runMainLooperUntil(
- () ->
- expectedTimeline.equals(player.getCurrentTimeline())
- || player.getPlayerError() != null);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
+ try {
+ run(player).untilTimelineChangesTo(expectedTimeline);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
- * 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.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilTimelineChanges()}.
*
* @param player The {@link Player}.
* @return The new {@link Timeline}.
@@ -153,30 +582,24 @@ public class TestPlayerRunHelper {
* exceeded.
*/
public static Timeline runUntilTimelineChanged(Player player) throws TimeoutException {
- verifyMainTestThread(player);
- 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);
- runMainLooperUntil(() -> receivedTimeline.get() != null || player.getPlayerError() != null);
- player.removeListener(listener);
- if (player.getPlayerError() != null) {
- throw new IllegalStateException(player.getPlayerError());
+ try {
+ return run(player).untilTimelineChanges();
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
- return checkNotNull(receivedTimeline.get());
}
/**
* 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 a playback error occurs.
+ * called with the specified {@link Player.DiscontinuityReason} or an error occurs.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilPositionDiscontinuityWithReason(int)}.
*
* @param player The {@link Player}.
* @param expectedReason The expected {@link Player.DiscontinuityReason}.
@@ -185,51 +608,40 @@ public class TestPlayerRunHelper {
*/
public static void runUntilPositionDiscontinuity(
Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
- verifyMainTestThread(player);
- if (player instanceof ExoPlayer) {
- verifyPlaybackThreadIsAlive((ExoPlayer) player);
- }
- AtomicBoolean receivedCallback = new AtomicBoolean(false);
- Player.Listener listener =
- 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());
+ try {
+ run(player).untilPositionDiscontinuityWithReason(expectedReason);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
* Runs tasks of the main {@link Looper} until a player error occurs.
*
+ * Non-fatal errors are ignored.
+ *
+ * New usages should prefer {@link #run(ExoPlayer)} and {@link
+ * ExoPlayerRunResult#untilPlayerError()}.
+ *
* @param player The {@link Player}.
* @return The raised {@link ExoPlaybackException}.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static ExoPlaybackException runUntilError(ExoPlayer player) throws TimeoutException {
- verifyMainTestThread(player);
- verifyPlaybackThreadIsAlive(player);
-
- runMainLooperUntil(() -> player.getPlayerError() != null);
- return checkNotNull(player.getPlayerError());
+ return run(player).untilPlayerError();
}
/**
- * Runs tasks of the main {@link Looper} until {@link
- * ExoPlayer.AudioOffloadListener#onSleepingForOffloadChanged(boolean)} is called or a playback
- * error occurs.
+ * Runs tasks of the main {@link Looper} until {@link ExoPlayer#isSleepingForOffload()} matches
+ * the expected value, or an error occurs.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(ExoPlayer)} and {@link
+ * ExoPlayerRunResult#untilSleepingForOffloadBecomes(boolean)}.
*
* @param player The {@link Player}.
* @param expectedSleepForOffload The expected sleep of offload state.
@@ -238,66 +650,51 @@ public class TestPlayerRunHelper {
*/
public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)
throws TimeoutException {
- verifyMainTestThread(player);
- verifyPlaybackThreadIsAlive(player);
-
- AtomicBoolean receiverCallback = new AtomicBoolean(false);
- ExoPlayer.AudioOffloadListener listener =
- new ExoPlayer.AudioOffloadListener() {
- @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());
+ try {
+ run(player).untilSleepingForOffloadBecomes(expectedSleepForOffload);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
- * Runs tasks of the main {@link Looper} until the {@link Player.Listener#onRenderedFirstFrame}
- * callback is called or a playback error occurs.
+ * Runs tasks of the main {@link Looper} until {@link Player.Listener#onRenderedFirstFrame} is
+ * called or an error occurs.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}..
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(Player)} and {@link
+ * PlayerRunResult#untilFirstFrameIsRendered()}.
*
* @param player The {@link Player}.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static void runUntilRenderedFirstFrame(ExoPlayer player) throws TimeoutException {
- verifyMainTestThread(player);
- verifyPlaybackThreadIsAlive(player);
-
- AtomicBoolean receivedCallback = new AtomicBoolean(false);
- Player.Listener listener =
- new Player.Listener() {
- @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());
+ try {
+ run(player).untilFirstFrameIsRendered();
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
- * Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
- * reaches the specified position or a playback error occurs.
+ * Calls {@link Player#play()} then runs tasks of the main {@link Looper} until the {@code player}
+ * reaches the specified position or an error occurs.
*
* 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
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(ExoPlayer)} and {@link
+ * ExoPlayerRunResult#untilPosition(int, long)}.
*
* @param player The {@link Player}.
* @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)
throws TimeoutException {
- verifyMainTestThread(player);
- verifyPlaybackThreadIsAlive(player);
- 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());
+ try {
+ play(player).untilPosition(mediaItemIndex, positionMs);
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
}
}
/**
- * 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.
*
* 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
- * RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
+ * media item and will only be unblocked by other {@code runUntil/playUntil...} methods, custom
+ * {@link RobolectricUtil#runMainLooperUntil} conditions or an explicit {@link
* ThreadTestUtil#unblockThreadsWaitingForProgressOnCurrentLooper()} on the main thread.
*
- * If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ * If a checked exception occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * New usages should prefer {@link #run(ExoPlayer)} and {@link
+ * ExoPlayerRunResult#untilStartOfMediaItem(int)}.
*
* @param player The {@link Player}.
* @param mediaItemIndex The index of the media item.
@@ -356,31 +734,34 @@ public class TestPlayerRunHelper {
*/
public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)
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
* commands on the internal playback thread.
*
+ * Both fatal and non-fatal errors are ignored.
+ *
* @param player The {@link Player}.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player)
throws TimeoutException {
- verifyMainTestThread(player);
- verifyPlaybackThreadIsAlive(player);
-
- // 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);
+ try {
+ run(player).untilPendingCommandsAreFullyHandled();
+ } catch (RuntimeException | TimeoutException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
}
private static void verifyMainTestThread(Player player) {
@@ -395,4 +776,92 @@ public class TestPlayerRunHelper {
player.getPlaybackLooper().getThread().isAlive(),
"Playback thread is not alive, has the player been released?");
}
+
+ /**
+ * A {@link Player.Listener} and {@link AnalyticsListener} that records errors.
+ *
+ * All methods must be called on {@link Player#getApplicationLooper()}.
+ */
+ private static final class ErrorListener implements AnalyticsListener, Player.Listener {
+
+ @Nullable private final List