blob: ea3c74f26ffefeca3e76e054a1a50af843b08866 [file] [log] [blame]
/*
* 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 com.google.android.exoplayer2.testutil;
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 com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.testutil.HostActivity.HostedTest;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.EventLogger;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link HostedTest} for {@link ExoPlayer} playback tests. */
public abstract class ExoHostedTest implements AnalyticsListener, 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 = 2000;
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 SimpleExoPlayer player;
private @MonotonicNonNull HandlerWrapper actionHandler;
private @MonotonicNonNull DefaultTrackSelector trackSelector;
private @MonotonicNonNull Surface surface;
private @MonotonicNonNull ExoPlaybackException playerError;
private boolean playerWasPrepared;
private boolean playing;
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(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(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);
String userAgent = "ExoPlayerPlaybackTests";
player = buildExoPlayer(host, surface, trackSelector);
player.play();
player.addAnalyticsListener(this);
player.addAnalyticsListener(new EventLogger(trackSelector, tag));
// Schedule any pending actions.
actionHandler = Clock.DEFAULT.createHandler(Util.getLooper(), /* callback= */ null);
if (pendingSchedule != null) {
pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null);
pendingSchedule = null;
}
DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent);
player.setMediaSource(
buildSource(
host, Util.getUserAgent(host, userAgent), 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 Error(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: " + totalPlayingTimeMs + ". Expected: " + playingTimeToAssertMs)
.that(
minAllowedActualPlayingTimeMs <= totalPlayingTimeMs
&& totalPlayingTimeMs <= maxAllowedActualPlayingTimeMs)
.isTrue();
}
// Make any additional assertions.
assertPassed(audioDecoderCounters, videoDecoderCounters);
}
// AnalyticsListener
@Override
public final void onPlayerStateChanged(
EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {
Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]");
playerWasPrepared |= playbackState != Player.STATE_IDLE;
if (playbackState == Player.STATE_ENDED
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
stopTest();
}
boolean playing = playWhenReady && playbackState == Player.STATE_READY;
if (!this.playing && playing) {
lastPlayingStartTimeMs = SystemClock.elapsedRealtime();
} else if (this.playing && !playing) {
totalPlayingTimeMs += SystemClock.elapsedRealtime() - lastPlayingStartTimeMs;
}
this.playing = playing;
}
@Override
public final void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
playerWasPrepared = true;
playerError = error;
onPlayerErrorInternal(error);
}
@Override
public void onDecoderDisabled(
EventTime eventTime, int trackType, DecoderCounters decoderCounters) {
if (trackType == C.TRACK_TYPE_AUDIO) {
audioDecoderCounters.merge(decoderCounters);
} else if (trackType == C.TRACK_TYPE_VIDEO) {
videoDecoderCounters.merge(decoderCounters);
}
}
// 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(String userAgent) {
// Do nothing. Interested subclasses may override.
return DrmSessionManager.getDummyDrmSessionManager();
}
protected DefaultTrackSelector buildTrackSelector(HostActivity host) {
return new DefaultTrackSelector(host);
}
protected SimpleExoPlayer buildExoPlayer(
HostActivity host, Surface surface, MappingTrackSelector trackSelector) {
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(host);
renderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
renderersFactory.setAllowedVideoJoiningTimeMs(/* allowedVideoJoiningTimeMs= */ 0);
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(host, renderersFactory)
.setTrackSelector(trackSelector)
.build();
player.setVideoSurface(surface);
return player;
}
protected abstract MediaSource buildSource(
HostActivity host,
String userAgent,
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;
}
}