public abstract class

ExoHostedTest

extends java.lang.Object

implements HostActivity.HostedTest

 java.lang.Object

↳androidx.media3.test.utils.ExoHostedTest

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-test-utils', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-test-utils
  • version: 1.5.0-alpha01

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

Overview

A HostActivity.HostedTest for ExoPlayer playback tests.

Summary

Fields
public static final longEXPECTED_PLAYING_TIME_MEDIA_DURATION_MS

public static final longEXPECTED_PLAYING_TIME_UNSET

public static final longMAX_PLAYING_TIME_DISCREPANCY_MS

protected final java.lang.Stringtag

Constructors
publicExoHostedTest(java.lang.String tag, boolean fullPlaybackNoSeeking)

publicExoHostedTest(java.lang.String tag, long expectedPlayingTimeMs, boolean failOnPlayerError)

Methods
protected voidassertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters)

public final booleanblockUntilStopped(long timeoutMs)

protected DrmSessionManagerbuildDrmSessionManager()

protected ExoPlayerbuildExoPlayer(HostActivity host, Surface surface, MappingTrackSelector trackSelector)

protected abstract MediaSourcebuildSource(HostActivity host, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout)

protected DefaultTrackSelectorbuildTrackSelector(HostActivity host)

public final booleanforceStop()

protected voidlogMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters)

public final voidonFinished()

protected voidonPlayerErrorInternal(ExoPlaybackException error)

public final voidonStart(HostActivity host, Surface surface, FrameLayout overlayFrameLayout)

public final voidsetSchedule(ActionSchedule schedule)

Sets a schedule to be applied during the test.

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

Fields

public static final long MAX_PLAYING_TIME_DISCREPANCY_MS

public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS

public static final long EXPECTED_PLAYING_TIME_UNSET

protected final java.lang.String tag

Constructors

public ExoHostedTest(java.lang.String tag, boolean fullPlaybackNoSeeking)

Parameters:

tag: A tag to use for logging.
fullPlaybackNoSeeking: Whether the test will play the target media in full without seeking. If set to true, the test will assert that the total time spent playing the media was within ExoHostedTest.MAX_PLAYING_TIME_DISCREPANCY_MS of the media duration. If set to false, the test will not assert an expected playing time.

public ExoHostedTest(java.lang.String tag, long expectedPlayingTimeMs, boolean failOnPlayerError)

Parameters:

tag: A tag to use for logging.
expectedPlayingTimeMs: The expected playing time. If set to a non-negative value, the test will assert that the total time spent playing the media was within ExoHostedTest.MAX_PLAYING_TIME_DISCREPANCY_MS of the specified value. ExoHostedTest.EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS should be passed to assert that the expected playing time equals the duration of the media being played. Else ExoHostedTest.EXPECTED_PLAYING_TIME_UNSET should be passed to indicate that the test should not assert an expected playing time.
failOnPlayerError: Whether a player error should be considered a test failure.

Methods

public final void setSchedule(ActionSchedule schedule)

Sets a schedule to be applied during the test.

Parameters:

schedule: The schedule.

public final void onStart(HostActivity host, Surface surface, FrameLayout overlayFrameLayout)

public final boolean blockUntilStopped(long timeoutMs)

public final boolean forceStop()

public final void onFinished()

protected DrmSessionManager buildDrmSessionManager()

protected DefaultTrackSelector buildTrackSelector(HostActivity host)

protected ExoPlayer buildExoPlayer(HostActivity host, Surface surface, MappingTrackSelector trackSelector)

protected abstract MediaSource buildSource(HostActivity host, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout)

protected void onPlayerErrorInternal(ExoPlaybackException error)

protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters)

protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters)

Source

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

import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertWithMessage;

import android.os.ConditionVariable;
import android.os.SystemClock;
import android.view.Surface;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import androidx.media3.common.Player;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.audio.DefaultAudioSink;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector;
import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.test.utils.HostActivity.HostedTest;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/** A {@link HostedTest} for {@link ExoPlayer} playback tests. */
@UnstableApi
public abstract class ExoHostedTest implements HostedTest {

  static {
    // DefaultAudioSink is able to work around spurious timestamps reported by the platform (by
    // ignoring them). Disable this workaround, since we're interested in testing that the
    // underlying platform is behaving correctly.
    DefaultAudioSink.failOnSpuriousAudioTimestamp = true;
  }

  public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 5000;
  public static final long EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS = -2;
  public static final long EXPECTED_PLAYING_TIME_UNSET = -1;

  protected final String tag;

  private final boolean failOnPlayerError;
  private final long expectedPlayingTimeMs;
  private final DecoderCounters videoDecoderCounters;
  private final DecoderCounters audioDecoderCounters;
  private final ConditionVariable testFinished;

  @Nullable private ActionSchedule pendingSchedule;
  private @MonotonicNonNull ExoPlayer player;
  private @MonotonicNonNull HandlerWrapper actionHandler;
  private @MonotonicNonNull DefaultTrackSelector trackSelector;
  private @MonotonicNonNull Surface surface;
  private @MonotonicNonNull ExoPlaybackException playerError;

  private long totalPlayingTimeMs;
  private long lastPlayingStartTimeMs;
  private long sourceDurationMs;

  /**
   * @param tag A tag to use for logging.
   * @param fullPlaybackNoSeeking Whether the test will play the target media in full without
   *     seeking. If set to true, the test will assert that the total time spent playing the media
   *     was within {@link #MAX_PLAYING_TIME_DISCREPANCY_MS} of the media duration. If set to false,
   *     the test will not assert an expected playing time.
   */
  public ExoHostedTest(@Size(max = 23) String tag, boolean fullPlaybackNoSeeking) {
    this(
        tag,
        fullPlaybackNoSeeking
            ? EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS
            : EXPECTED_PLAYING_TIME_UNSET,
        true);
  }

  /**
   * @param tag A tag to use for logging.
   * @param expectedPlayingTimeMs The expected playing time. If set to a non-negative value, the
   *     test will assert that the total time spent playing the media was within {@link
   *     #MAX_PLAYING_TIME_DISCREPANCY_MS} of the specified value. {@link
   *     #EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS} should be passed to assert that the expected
   *     playing time equals the duration of the media being played. Else {@link
   *     #EXPECTED_PLAYING_TIME_UNSET} should be passed to indicate that the test should not assert
   *     an expected playing time.
   * @param failOnPlayerError Whether a player error should be considered a test failure.
   */
  public ExoHostedTest(
      @Size(max = 23) String tag, long expectedPlayingTimeMs, boolean failOnPlayerError) {
    this.tag = tag;
    this.expectedPlayingTimeMs = expectedPlayingTimeMs;
    this.failOnPlayerError = failOnPlayerError;
    this.testFinished = new ConditionVariable();
    this.videoDecoderCounters = new DecoderCounters();
    this.audioDecoderCounters = new DecoderCounters();
  }

  /**
   * Sets a schedule to be applied during the test.
   *
   * @param schedule The schedule.
   */
  public final void setSchedule(ActionSchedule schedule) {
    if (!isStarted()) {
      pendingSchedule = schedule;
    } else {
      schedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null);
    }
  }

  // HostedTest implementation

  @Override
  public final void onStart(HostActivity host, Surface surface, FrameLayout overlayFrameLayout) {
    this.surface = surface;
    // Build the player.
    trackSelector = buildTrackSelector(host);
    player = buildExoPlayer(host, surface, trackSelector);
    player.play();
    player.addAnalyticsListener(new AnalyticsListenerImpl());
    player.addAnalyticsListener(new EventLogger(tag));
    // Schedule any pending actions.
    actionHandler =
        Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null);
    if (pendingSchedule != null) {
      pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null);
      pendingSchedule = null;
    }
    DrmSessionManager drmSessionManager = buildDrmSessionManager();
    player.setMediaSource(buildSource(host, drmSessionManager, overlayFrameLayout));
    player.prepare();
  }

  @Override
  public final boolean blockUntilStopped(long timeoutMs) {
    return testFinished.block(timeoutMs);
  }

  @Override
  public final boolean forceStop() {
    return stopTest();
  }

  @Override
  public final void onFinished() {
    if (failOnPlayerError && playerError != null) {
      throw new RuntimeException(playerError);
    }
    logMetrics(audioDecoderCounters, videoDecoderCounters);
    if (expectedPlayingTimeMs != EXPECTED_PLAYING_TIME_UNSET) {
      long playingTimeToAssertMs =
          expectedPlayingTimeMs == EXPECTED_PLAYING_TIME_MEDIA_DURATION_MS
              ? sourceDurationMs
              : expectedPlayingTimeMs;
      // Assert that the playback spanned the correct duration of time.
      long minAllowedActualPlayingTimeMs = playingTimeToAssertMs - MAX_PLAYING_TIME_DISCREPANCY_MS;
      long maxAllowedActualPlayingTimeMs = playingTimeToAssertMs + MAX_PLAYING_TIME_DISCREPANCY_MS;
      assertWithMessage(
              "Total playing time: %sms. Expected: %sms", totalPlayingTimeMs, playingTimeToAssertMs)
          .that(
              minAllowedActualPlayingTimeMs <= totalPlayingTimeMs
                  && totalPlayingTimeMs <= maxAllowedActualPlayingTimeMs)
          .isTrue();
    }
    // Make any additional assertions.
    assertPassed(audioDecoderCounters, videoDecoderCounters);
  }

  // Internal logic

  private boolean stopTest() {
    if (!isStarted()) {
      return false;
    }
    actionHandler.removeCallbacksAndMessages(null);
    sourceDurationMs = player.getDuration();
    player.release();
    // We post opening of the finished condition so that any events posted to the main thread as a
    // result of player.release() are guaranteed to be handled before the test returns.
    actionHandler.post(testFinished::open);
    return true;
  }

  protected DrmSessionManager buildDrmSessionManager() {
    // Do nothing. Interested subclasses may override.
    return DrmSessionManager.DRM_UNSUPPORTED;
  }

  protected DefaultTrackSelector buildTrackSelector(HostActivity host) {
    return new DefaultTrackSelector(host);
  }

  protected ExoPlayer buildExoPlayer(
      HostActivity host, Surface surface, MappingTrackSelector trackSelector) {
    DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(host);
    renderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
    renderersFactory.setAllowedVideoJoiningTimeMs(/* allowedVideoJoiningTimeMs= */ 0);
    ExoPlayer player =
        new ExoPlayer.Builder(host, renderersFactory).setTrackSelector(trackSelector).build();
    player.setVideoSurface(surface);
    return player;
  }

  protected abstract MediaSource buildSource(
      HostActivity host, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout);

  protected void onPlayerErrorInternal(ExoPlaybackException error) {
    // Do nothing. Interested subclasses may override.
  }

  protected void logMetrics(DecoderCounters audioCounters, DecoderCounters videoCounters) {
    // Do nothing. Subclasses may override to log metrics.
  }

  protected void assertPassed(DecoderCounters audioCounters, DecoderCounters videoCounters) {
    // Do nothing. Subclasses may override to add additional assertions.
  }

  @EnsuresNonNullIf(
      result = true,
      expression = {"player", "actionHandler", "trackSelector", "surface"})
  private boolean isStarted() {
    if (player == null) {
      return false;
    }
    Util.castNonNull(actionHandler);
    Util.castNonNull(trackSelector);
    Util.castNonNull(surface);
    return true;
  }

  private final class AnalyticsListenerImpl implements AnalyticsListener {
    @Override
    public void onEvents(Player player, Events events) {
      if (events.contains(EVENT_IS_PLAYING_CHANGED)) {
        if (player.isPlaying()) {
          lastPlayingStartTimeMs = SystemClock.elapsedRealtime();
        } else {
          totalPlayingTimeMs += SystemClock.elapsedRealtime() - lastPlayingStartTimeMs;
        }
      }
      if (events.contains(EVENT_PLAYER_ERROR)) {
        // The exception is guaranteed to be an ExoPlaybackException because the underlying player
        // is an ExoPlayer instance.
        playerError = (ExoPlaybackException) checkNotNull(player.getPlayerError());
        onPlayerErrorInternal(playerError);
      }
      if (events.contains(EVENT_PLAYBACK_STATE_CHANGED)) {
        @Player.State int playbackState = player.getPlaybackState();
        if (playbackState == Player.STATE_ENDED || playbackState == Player.STATE_IDLE) {
          stopTest();
        }
      }
    }

    @Override
    public void onAudioDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
      audioDecoderCounters.merge(decoderCounters);
    }

    @Override
    public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
      videoDecoderCounters.merge(decoderCounters);
    }
  }
}