public class

TestPlayerRunHelper

extends java.lang.Object

 java.lang.Object

↳androidx.media3.test.utils.robolectric.TestPlayerRunHelper

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-test-utils-robolectric', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-test-utils-robolectric
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-test-utils-robolectric:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

Helper methods to block the calling thread until the provided ExoPlayer instance reaches a particular state.

Summary

Methods
public static voidplayUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs)

Calls Player.play(), runs tasks of the main until the player reaches the specified position or a playback error occurs, and then pauses the player.

public static voidplayUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)

Calls Player.play(), runs tasks of the main until the player reaches the specified media item or a playback error occurs, and then pauses the player.

public static ExoPlaybackExceptionrunUntilError(ExoPlayer player)

Runs tasks of the main until a player error occurs.

public static voidrunUntilPendingCommandsAreFullyHandled(ExoPlayer player)

Runs tasks of the main until the player completely handled all previously issued commands on the internal playback thread.

public static voidrunUntilPlaybackState(Player player, int expectedState)

Runs tasks of the main until Player.getPlaybackState() matches the expected state or a playback error occurs.

public static voidrunUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)

Runs tasks of the main until Player.getPlayWhenReady() matches the expected value or a playback error occurs.

public static voidrunUntilPositionDiscontinuity(Player player, int expectedReason)

Runs tasks of the main until is called with the specified or a playback error occurs.

public static booleanrunUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)

Runs tasks of the main until is called or a playback error occurs.

public static voidrunUntilRenderedFirstFrame(ExoPlayer player)

Runs tasks of the main until the callback is called or a playback error occurs.

public static voidrunUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)

Runs tasks of the main until is called or a playback error occurs.

public static TimelinerunUntilTimelineChanged(Player player)

Runs tasks of the main until a timeline change or a playback error occurs.

public static voidrunUntilTimelineChanged(Player player, Timeline expectedTimeline)

Runs tasks of the main until Player.getCurrentTimeline() matches the expected timeline or a playback error occurs.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static void runUntilPlaybackState(Player player, int expectedState)

Runs tasks of the main until Player.getPlaybackState() matches the expected state or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
expectedState: The expected .

public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)

Runs tasks of the main until Player.getPlayWhenReady() matches the expected value or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
expectedPlayWhenReady: The expected value for Player.getPlayWhenReady().

public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline)

Runs tasks of the main until Player.getCurrentTimeline() matches the expected timeline or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
expectedTimeline: The expected Timeline.

public static Timeline runUntilTimelineChanged(Player player)

Runs tasks of the main until a timeline change or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.

Returns:

The new Timeline.

public static void runUntilPositionDiscontinuity(Player player, int expectedReason)

Runs tasks of the main until is called with the specified or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
expectedReason: The expected .

public static ExoPlaybackException runUntilError(ExoPlayer player)

Runs tasks of the main until a player error occurs.

Parameters:

player: The Player.

Returns:

The raised ExoPlaybackException.

public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)

Runs tasks of the main until is called or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.

Returns:

The new offloadSchedulingEnabled state.

public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)

Runs tasks of the main until is called or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
expectedSleepForOffload: The expected sleep of offload state.

public static void runUntilRenderedFirstFrame(ExoPlayer player)

Runs tasks of the main until the callback is called or a playback error occurs.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException..

Parameters:

player: The Player.

public static void playUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs)

Calls Player.play(), runs tasks of the main until the player reaches the specified position or a playback error occurs, and then pauses the player.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
mediaItemIndex: The index of the media item.
positionMs: The position within the media item, in milliseconds.

public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)

Calls Player.play(), runs tasks of the main until the player reaches the specified media item or a playback error occurs, and then pauses the player.

If a playback error occurs it will be thrown wrapped in an java.lang.IllegalStateException.

Parameters:

player: The Player.
mediaItemIndex: The index of the media item.

public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player)

Runs tasks of the main until the player completely handled all previously issued commands on the internal playback thread.

Parameters:

player: The Player.

Source

/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media3.test.utils.robolectric;

import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;

import android.os.Looper;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.ExoPlayer;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/**
 * Helper methods to block the calling thread until the provided {@link ExoPlayer} instance reaches
 * a particular state.
 */
@UnstableApi
public class TestPlayerRunHelper {

  private TestPlayerRunHelper() {}

  /**
   * Runs tasks of the main {@link Looper} until {@link Player#getPlaybackState()} matches the
   * expected state or a playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param expectedState The expected {@link Player.State}.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void runUntilPlaybackState(Player player, @Player.State int expectedState)
      throws TimeoutException {
    verifyMainTestThread(player);
    runMainLooperUntil(
        () -> player.getPlaybackState() == expectedState || player.getPlayerError() != null);
    if (player.getPlayerError() != null) {
      throw new IllegalStateException(player.getPlayerError());
    }
  }

  /**
   * Runs tasks of the main {@link Looper} until {@link Player#getPlayWhenReady()} matches the
   * expected value or a playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param expectedPlayWhenReady The expected value for {@link Player#getPlayWhenReady()}.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)
      throws TimeoutException {
    verifyMainTestThread(player);
    runMainLooperUntil(
        () ->
            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#getCurrentTimeline()} matches the
   * expected timeline or a playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param expectedTimeline The expected {@link Timeline}.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void runUntilTimelineChanged(Player player, Timeline expectedTimeline)
      throws TimeoutException {
    verifyMainTestThread(player);
    runMainLooperUntil(
        () ->
            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.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @return The new {@link Timeline}.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     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());
    }
    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.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param expectedReason The expected {@link Player.DiscontinuityReason}.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void runUntilPositionDiscontinuity(
      Player player, @Player.DiscontinuityReason int expectedReason) throws TimeoutException {
    verifyMainTestThread(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());
    }
  }

  /**
   * Runs tasks of the main {@link Looper} until a player error occurs.
   *
   * @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);
    runMainLooperUntil(() -> player.getPlayerError() != null);
    return checkNotNull(player.getPlayerError());
  }

  /**
   * Runs tasks of the main {@link Looper} until {@link
   * ExoPlayer.AudioOffloadListener#onExperimentalOffloadSchedulingEnabledChanged} is called or a
   * playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @return The new offloadSchedulingEnabled state.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(ExoPlayer player)
      throws TimeoutException {
    verifyMainTestThread(player);
    AtomicReference<@NullableType Boolean> offloadSchedulingEnabledReceiver =
        new AtomicReference<>();
    ExoPlayer.AudioOffloadListener listener =
        new ExoPlayer.AudioOffloadListener() {
          @Override
          public void onExperimentalOffloadSchedulingEnabledChanged(
              boolean offloadSchedulingEnabled) {
            offloadSchedulingEnabledReceiver.set(offloadSchedulingEnabled);
          }
        };
    player.addAudioOffloadListener(listener);
    runMainLooperUntil(
        () -> offloadSchedulingEnabledReceiver.get() != null || player.getPlayerError() != null);
    player.removeAudioOffloadListener(listener);
    if (player.getPlayerError() != null) {
      throw new IllegalStateException(player.getPlayerError());
    }
    return checkNotNull(offloadSchedulingEnabledReceiver.get());
  }

  /**
   * Runs tasks of the main {@link Looper} until {@link
   * ExoPlayer.AudioOffloadListener#onExperimentalSleepingForOffloadChanged(boolean)} is called or a
   * playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param expectedSleepForOffload The expected sleep of offload state.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void runUntilSleepingForOffload(ExoPlayer player, boolean expectedSleepForOffload)
      throws TimeoutException {
    verifyMainTestThread(player);
    AtomicBoolean receiverCallback = new AtomicBoolean(false);
    ExoPlayer.AudioOffloadListener listener =
        new ExoPlayer.AudioOffloadListener() {
          @Override
          public void onExperimentalSleepingForOffloadChanged(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}
   * callback is called or a playback error occurs.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}..
   *
   * @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);
    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());
    }
  }

  /**
   * Calls {@link Player#play()}, runs tasks of the main {@link Looper} until the {@code player}
   * reaches the specified position or a playback error occurs, and then pauses the {@code player}.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param mediaItemIndex The index of the media item.
   * @param positionMs The position within the media item, in milliseconds.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void playUntilPosition(ExoPlayer player, int mediaItemIndex, long positionMs)
      throws TimeoutException {
    verifyMainTestThread(player);
    Looper applicationLooper = Util.getCurrentOrMainLooper();
    AtomicBoolean messageHandled = new AtomicBoolean(false);
    player
        .createMessage(
            (messageType, payload) -> {
              // Block playback thread until pause command has been sent from test thread.
              ConditionVariable blockPlaybackThreadCondition = new ConditionVariable();
              player
                  .getClock()
                  .createHandler(applicationLooper, /* callback= */ null)
                  .post(
                      () -> {
                        player.pause();
                        messageHandled.set(true);
                        blockPlaybackThreadCondition.open();
                      });
              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}
   * reaches the specified media item or a playback error occurs, and then pauses the {@code
   * player}.
   *
   * <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
   *
   * @param player The {@link Player}.
   * @param mediaItemIndex The index of the media item.
   * @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
   *     exceeded.
   */
  public static void playUntilStartOfMediaItem(ExoPlayer player, int mediaItemIndex)
      throws TimeoutException {
    playUntilPosition(player, mediaItemIndex, /* positionMs= */ 0);
  }

  /**
   * Runs tasks of the main {@link Looper} until the player completely handled all previously issued
   * commands on the internal playback thread.
   *
   * @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);
    // 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);
  }

  private static void verifyMainTestThread(Player player) {
    if (Looper.myLooper() != Looper.getMainLooper()
        || player.getApplicationLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException();
    }
  }
}