Remove flakiness from DefaultAnalyticsCollectorTest

Our FakeClock generally makes sure that playback tests are fully
deterministic. However, this fails if the test uses blocking waits
with clock.onThreadBlocked and where relevant Handlers are created
without using the clock.

To fix the flakiness, we can make the following adjustments:
 - Use TestExoPlayerBuilder instead of legacy ExoPlayerTestRunner
   to avoid onThreadBlocked calls. This also makes the tests more
   readable.
 - Use clock to create Handler for FakeVideoRenderer and
   FakeAudioRenderer. Ideally, this should be passed through
   RenderersFactory, but it's too disruptive given this is a
   public API.
 - Use clock for MediaSourceList and MediaPeriodQueue update
   handler.

PiperOrigin-RevId: 490907495
This commit is contained in:
tonihei 2022-11-25 15:36:22 +00:00 committed by Rohit Singh
parent fed53362c9
commit 6abc94a8b7
11 changed files with 570 additions and 425 deletions

View File

@ -281,7 +281,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
deliverPendingMessageAtStartPositionRequired = true;
Handler eventHandler = new Handler(applicationLooper);
HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);

View File

@ -26,6 +26,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player.RepeatMode;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
@ -71,7 +72,7 @@ import com.google.common.collect.ImmutableList;
private final Timeline.Period period;
private final Timeline.Window window;
private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler;
private final HandlerWrapper analyticsCollectorHandler;
private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode;
@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
* on.
*/
public MediaPeriodQueue(
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period();

View File

@ -15,13 +15,16 @@
*/
package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.os.Handler;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
@ -48,6 +51,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/**
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
@ -77,11 +81,10 @@ import java.util.Set;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
private final Set<MediaSourceHolder> enabledMediaSourceHolders;
private final AnalyticsCollector eventListener;
private final HandlerWrapper eventHandler;
private ShuffleOrder shuffleOrder;
private boolean isPrepared;
@ -101,7 +104,7 @@ import java.util.Set;
public MediaSourceList(
MediaSourceListInfoRefreshListener listener,
AnalyticsCollector analyticsCollector,
Handler analyticsCollectorHandler,
HandlerWrapper analyticsCollectorHandler,
PlayerId playerId) {
this.playerId = playerId;
mediaSourceListInfoListener = listener;
@ -109,12 +112,10 @@ import java.util.Set;
mediaSourceByMediaPeriod = new IdentityHashMap<>();
mediaSourceByUid = new HashMap<>();
mediaSourceHolders = new ArrayList<>();
mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher();
drmEventDispatcher = new DrmSessionEventListener.EventDispatcher();
eventListener = analyticsCollector;
eventHandler = analyticsCollectorHandler;
childSources = new HashMap<>();
enabledMediaSourceHolders = new HashSet<>();
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
}
/**
@ -308,7 +309,7 @@ import java.util.Set;
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaSource.MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
MediaSourceHolder holder = checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
enableMediaSource(holder);
holder.activeMediaPeriodIds.add(childMediaPeriodId);
MediaPeriod mediaPeriod =
@ -324,8 +325,7 @@ import java.util.Set;
* @param mediaPeriod The period to release.
*/
public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder =
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
holder.mediaSource.releasePeriod(mediaPeriod);
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
if (!mediaSourceByMediaPeriod.isEmpty()) {
@ -450,8 +450,7 @@ import java.util.Set;
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
// Release if the source has been removed from the playlist and no periods are still active.
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
MediaSourceAndListener removedChild =
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
MediaSourceAndListener removedChild = checkNotNull(childSources.remove(mediaSourceHolder));
removedChild.mediaSource.releaseSource(removedChild.caller);
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener);
@ -526,12 +525,8 @@ import java.util.Set;
implements MediaSourceEventListener, DrmSessionEventListener {
private final MediaSourceList.MediaSourceHolder id;
private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private DrmSessionEventListener.EventDispatcher drmEventDispatcher;
public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher;
drmEventDispatcher = MediaSourceList.this.drmEventDispatcher;
this.id = id;
}
@ -543,8 +538,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadStarted(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
}
}
@ -554,8 +555,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadCompleted(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
}
}
@ -565,8 +572,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadCanceled(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
}
}
@ -578,8 +591,19 @@ import java.util.Set;
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadError(
eventParameters.first,
eventParameters.second,
loadEventData,
mediaLoadData,
error,
wasCanceled));
}
}
@ -588,8 +612,14 @@ import java.util.Set;
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onUpstreamDiscarded(
eventParameters.first, checkNotNull(eventParameters.second), mediaLoadData));
}
}
@ -598,8 +628,14 @@ import java.util.Set;
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDownstreamFormatChanged(
eventParameters.first, eventParameters.second, mediaLoadData));
}
}
@ -610,75 +646,94 @@ import java.util.Set;
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
@DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionAcquired(state);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionAcquired(
eventParameters.first, eventParameters.second, state));
}
}
@Override
public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysLoaded();
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysLoaded(eventParameters.first, eventParameters.second));
}
}
@Override
public void onDrmSessionManagerError(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionManagerError(error);
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionManagerError(
eventParameters.first, eventParameters.second, error));
}
}
@Override
public void onDrmKeysRestored(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysRestored();
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysRestored(eventParameters.first, eventParameters.second));
}
}
@Override
public void onDrmKeysRemoved(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysRemoved();
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysRemoved(eventParameters.first, eventParameters.second));
}
}
@Override
public void onDrmSessionReleased(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionReleased();
@Nullable
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionReleased(eventParameters.first, eventParameters.second));
}
}
/** Updates the event dispatcher and returns whether the event should be dispatched. */
private boolean maybeUpdateEventDispatcher(
/** Updates the event parameters and returns whether the event should be dispatched. */
@Nullable
private Pair<Integer, MediaSource.@NullableType MediaPeriodId> getEventParameters(
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
if (childMediaPeriodId != null) {
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
if (mediaPeriodId == null) {
// Media period not found. Ignore event.
return false;
return null;
}
}
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
if (mediaSourceEventDispatcher.windowIndex != windowIndex
|| !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) {
mediaSourceEventDispatcher =
MediaSourceList.this.mediaSourceEventDispatcher.withParameters(
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
}
if (drmEventDispatcher.windowIndex != windowIndex
|| !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) {
drmEventDispatcher =
MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId);
}
return true;
return Pair.create(windowIndex, mediaPeriodId);
}
}
}

View File

@ -112,6 +112,7 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
@ -11897,7 +11898,11 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(context)
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> {
videoRenderer.set(new FakeVideoRenderer(handler, videoListener));
videoRenderer.set(
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener));
return new Renderer[] {videoRenderer.get()};
})
.build();
@ -12034,7 +12039,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build();
AnalyticsListener listener = mock(AnalyticsListener.class);
player.addAnalyticsListener(listener);
@ -12059,7 +12069,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);

View File

@ -25,7 +25,6 @@ import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import androidx.media3.common.AdPlaybackState;
@ -36,6 +35,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId;
@ -97,13 +97,14 @@ public final class MediaPeriodQueueTest {
analyticsCollector.setPlayer(
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(),
Looper.getMainLooper());
mediaPeriodQueue =
new MediaPeriodQueue(analyticsCollector, new Handler(Looper.getMainLooper()));
HandlerWrapper handler =
Clock.DEFAULT.createHandler(Looper.getMainLooper(), /* callback= */ null);
mediaPeriodQueue = new MediaPeriodQueue(analyticsCollector, handler);
mediaSourceList =
new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
analyticsCollector,
new Handler(Looper.getMainLooper()),
handler,
PlayerId.UNSET);
rendererCapabilities = new RendererCapabilities[0];
trackSelector = mock(TrackSelector.class);

View File

@ -67,7 +67,7 @@ public class MediaSourceListTest {
new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
analyticsCollector,
Util.createHandlerForCurrentOrMainLooper(),
Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null),
PlayerId.UNSET);
}

View File

@ -49,6 +49,12 @@ import static androidx.media3.exoplayer.analytics.AnalyticsListener.EVENT_VIDEO_
import static androidx.media3.exoplayer.analytics.AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilIsLoading;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@ -63,6 +69,8 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.robolectric.shadows.ShadowLooper.idleMainLooper;
import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask;
import android.graphics.SurfaceTexture;
import android.os.Looper;
@ -85,6 +93,7 @@ import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException;
@ -102,8 +111,6 @@ import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.test.utils.ActionSchedule;
import androidx.media3.test.utils.ActionSchedule.PlayerRunnable;
import androidx.media3.test.utils.ExoPlayerTestRunner;
import androidx.media3.test.utils.FakeAudioRenderer;
import androidx.media3.test.utils.FakeClock;
@ -132,14 +139,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.robolectric.shadows.ShadowLooper;
/** Integration test for {@link DefaultAnalyticsCollector}. */
@RunWith(AndroidJUnit4.class)
public final class DefaultAnalyticsCollectorTest {
private static final String TAG = "DefaultAnalyticsCollectorTest";
// Deprecated event constants.
private static final long EVENT_PLAYER_STATE_CHANGED = 1L << 63;
private static final long EVENT_SEEK_STARTED = 1L << 62;
@ -167,7 +171,6 @@ public final class DefaultAnalyticsCollectorTest {
private static final Format VIDEO_FORMAT_DRM_1 =
ExoPlayerTestRunner.VIDEO_FORMAT.buildUpon().setDrmInitData(DRM_DATA_1).build();
private static final int TIMEOUT_MS = 10_000;
private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline();
private static final EventWindowAndPeriodId WINDOW_0 =
new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null);
@ -217,7 +220,14 @@ public final class DefaultAnalyticsCollectorTest {
FakeMediaSource mediaSource =
new FakeMediaSource(
Timeline.EMPTY, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
@ -236,7 +246,14 @@ public final class DefaultAnalyticsCollectorTest {
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
@ -247,7 +264,7 @@ public final class DefaultAnalyticsCollectorTest {
period0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */)
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */)
.inOrder();
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */)
@ -297,7 +314,14 @@ public final class DefaultAnalyticsCollectorTest {
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
@ -378,7 +402,14 @@ public final class DefaultAnalyticsCollectorTest {
new ConcatenatingMediaSource(
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
@ -449,23 +480,23 @@ public final class DefaultAnalyticsCollectorTest {
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
// Wait until second period has fully loaded to assert loading events without flakiness.
.waitForIsLoading(true)
.waitForIsLoading(false)
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
// Wait until second period has fully loaded to assert loading events.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
runUntilIsLoading(player, /* expectedIsLoading= */ false);
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* BUFFERING */,
WINDOW_0 /* setPlayWhenReady=false */,
period0 /* READY */,
period1 /* BUFFERING */,
period1 /* setPlayWhenReady=true */,
@ -542,23 +573,24 @@ public final class DefaultAnalyticsCollectorTest {
SINGLE_PERIOD_TIMELINE,
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT));
long periodDurationMs =
long windowDurationMs =
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* mediaItemIndex= */ 0, periodDurationMs)
.seekAndWait(/* positionMs= */ 0)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
playUntilPosition(player, /* mediaItemIndex= */ 0, windowDurationMs - 100);
player.seekTo(/* positionMs= */ 0);
runUntilPlaybackState(player, Player.STATE_READY);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
period0 /* READY */,
period0 /* setPlayWhenReady=true */,
@ -653,17 +685,19 @@ public final class DefaultAnalyticsCollectorTest {
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
MediaSource mediaSource2 =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.setMediaSources(/* resetPosition= */ false, mediaSource2)
.waitForTimelineChanged()
// Wait until loading started to prevent flakiness caused by loading finishing too fast.
.waitForIsLoading(true)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(mediaSource1);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.setMediaSource(mediaSource2, /* resetPosition= */ false);
runUntilTimelineChanged(player);
// Wait until loading started to assert loading events.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
// Populate all event ids with last timeline (after second prepare).
populateEventIds(listener.lastReportedTimeline);
@ -676,9 +710,7 @@ public final class DefaultAnalyticsCollectorTest {
/* windowSequenceNumber= */ 0));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* BUFFERING */,
WINDOW_0 /* setPlayWhenReady=false */,
period0Seq0 /* READY */,
WINDOW_0 /* BUFFERING */,
period0Seq1 /* setPlayWhenReady=true */,
@ -688,9 +720,9 @@ public final class DefaultAnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGE */,
period0Seq0 /* SOURCE_UPDATE */,
WINDOW_0 /* SOURCE_UPDATE */,
WINDOW_0 /* PLAYLIST_CHANGE */,
period0Seq1 /* SOURCE_UPDATE */);
WINDOW_0 /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(WINDOW_0 /* REMOVE */);
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
@ -753,28 +785,31 @@ public final class DefaultAnalyticsCollectorTest {
public void reprepareAfterError() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(
ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
.waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 0)
.prepare()
// Wait until loading started to assert loading events without flakiness.
.waitForIsLoading(true)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player
.createMessage(
(message, payload) -> {
throw ExoPlaybackException.createForSource(
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
})
.send();
runUntilError(player);
player.seekTo(/* positionMs= */ 0);
player.prepare();
// Wait until loading started to assert loading events.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
period0Seq0 /* READY */,
period0Seq0 /* IDLE */,
@ -784,7 +819,7 @@ public final class DefaultAnalyticsCollectorTest {
period0Seq0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */);
.containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0);
@ -835,36 +870,33 @@ public final class DefaultAnalyticsCollectorTest {
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
final ConcatenatingMediaSource concatenatedMediaSource =
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
long periodDurationMs =
long windowDurationMs =
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
// Ensure second period is already being read from.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ periodDurationMs)
.executeRunnable(
() ->
concatenatedMediaSource.moveMediaSource(
/* currentIndex= */ 0, /* newIndex= */ 1))
.waitForTimelineChanged()
.waitForPlaybackState(Player.STATE_READY)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(concatenatedMediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
// Ensure second period is already being read from.
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ windowDurationMs - 100);
concatenatedMediaSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1);
runUntilTimelineChanged(player);
runUntilPlaybackState(player, Player.STATE_READY);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
window0Period1Seq0 /* READY */,
window0Period1Seq0 /* setPlayWhenReady=true */,
window0Period1Seq0 /* setPlayWhenReady=false */,
period1Seq0 /* setPlayWhenReady=true */,
period1Seq0 /* BUFFERING */,
period1Seq0 /* READY */,
period1Seq0 /* setPlayWhenReady=true */,
period1Seq0 /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
@ -926,20 +958,22 @@ public final class DefaultAnalyticsCollectorTest {
public void playlistOperations() throws Exception {
MediaSource fakeMediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.addMediaSources(fakeMediaSource)
// Wait until second period has fully loaded to assert loading events without flakiness.
.waitForIsLoading(true)
.waitForIsLoading(false)
.removeMediaItem(/* index= */ 0)
.waitForPlaybackState(Player.STATE_BUFFERING)
.waitForPlaybackState(Player.STATE_READY)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(fakeMediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.addMediaSource(fakeMediaSource);
// Wait until second period has fully loaded to assert loading events.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
runUntilIsLoading(player, /* expectedIsLoading= */ false);
player.removeMediaItem(/* index= */ 0);
runUntilPlaybackState(player, Player.STATE_BUFFERING);
runUntilPlaybackState(player, Player.STATE_READY);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
// Populate event ids with second to last timeline that still contained both periods.
populateEventIds(listener.reportedTimelines.get(listener.reportedTimelines.size() - 2));
@ -953,8 +987,6 @@ public final class DefaultAnalyticsCollectorTest {
/* windowSequenceNumber= */ 1));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
period0Seq0 /* READY */,
period0Seq1 /* BUFFERING */,
@ -965,7 +997,7 @@ public final class DefaultAnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
period0Seq0 /* SOURCE_UPDATE (first item) */,
WINDOW_0 /* SOURCE_UPDATE (first item) */,
period0Seq0 /* PLAYLIST_CHANGED (add) */,
period0Seq0 /* SOURCE_UPDATE (second item) */,
period0Seq1 /* PLAYLIST_CHANGED (remove) */)
@ -1063,60 +1095,53 @@ public final class DefaultAnalyticsCollectorTest {
}
},
ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.addListener(
new Player.Listener() {
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (!player.isPlayingAd()
&& reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
// Finished playing ad. Marked as played.
adPlaybackState.set(
adPlaybackState
.get()
.withPlayedAd(
/* adGroupIndex= */ playedAdCount.getAndIncrement(),
/* adIndexInAdGroup= */ 0));
fakeMediaSource.setNewSourceInfo(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
contentDurationsUs,
adPlaybackState.get())),
/* sendManifestLoadEvents= */ false);
}
}
});
}
})
.pause()
// Ensure everything is preloaded.
.waitForIsLoading(true)
.waitForIsLoading(false)
.waitForPlaybackState(Player.STATE_READY)
// Wait in each content part to ensure previously triggered events get a chance to be
// delivered. This prevents flakiness caused by playback progressing too fast.
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 3_000)
.waitForPendingPlayerCommands()
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8_000)
.waitForPendingPlayerCommands()
.play()
.waitForPlaybackState(Player.STATE_ENDED)
// Wait for final timeline change that marks post-roll played.
.waitForTimelineChanged()
.build();
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
player.addListener(
new Player.Listener() {
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (!player.isPlayingAd() && reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
// Finished playing ad. Marked as played.
adPlaybackState.set(
adPlaybackState
.get()
.withPlayedAd(
/* adGroupIndex= */ playedAdCount.getAndIncrement(),
/* adIndexInAdGroup= */ 0));
fakeMediaSource.setNewSourceInfo(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
contentDurationsUs,
adPlaybackState.get())),
/* sendManifestLoadEvents= */ false);
}
}
});
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(fakeMediaSource);
player.prepare();
// Ensure everything is preloaded.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
runUntilIsLoading(player, /* expectedIsLoading= */ false);
runUntilPlaybackState(player, Player.STATE_READY);
// Wait in each content part to ensure previously triggered events get a chance to be delivered.
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 3_000);
runUntilPendingCommandsAreFullyHandled(player);
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 8_000);
runUntilPendingCommandsAreFullyHandled(player);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
// Wait for final timeline change that marks post-roll played.
runUntilTimelineChanged(player);
Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0);
EventWindowAndPeriodId prerollAd =
@ -1158,8 +1183,6 @@ public final class DefaultAnalyticsCollectorTest {
periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
prerollAd /* READY */,
prerollAd /* setPlayWhenReady=true */,
@ -1172,7 +1195,7 @@ public final class DefaultAnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
prerollAd /* SOURCE_UPDATE (initial) */,
WINDOW_0 /* SOURCE_UPDATE (initial) */,
contentAfterPreroll /* SOURCE_UPDATE (played preroll) */,
contentAfterMidroll /* SOURCE_UPDATE (played midroll) */,
contentAfterPostroll /* SOURCE_UPDATE (played postroll) */)
@ -1322,20 +1345,21 @@ public final class DefaultAnalyticsCollectorTest {
}
},
ExoPlayerTestRunner.VIDEO_FORMAT);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
// Ensure everything is preloaded.
.waitForIsLoading(true)
.waitForIsLoading(false)
// Seek behind the midroll.
.seek(6 * C.MICROS_PER_SECOND)
// Wait until loading started again to assert loading events without flakiness.
.waitForIsLoading(true)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.build();
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(fakeMediaSource);
player.prepare();
// Ensure everything is preloaded.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
runUntilIsLoading(player, /* expectedIsLoading= */ false);
// Seek behind the midroll.
player.seekTo(/* positionMs= */ 6_000);
// Wait until loading started again to assert loading events.
runUntilIsLoading(player, /* expectedIsLoading= */ true);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0);
EventWindowAndPeriodId midrollAd =
@ -1357,8 +1381,6 @@ public final class DefaultAnalyticsCollectorTest {
periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET));
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
.containsExactly(
WINDOW_0 /* setPlayWhenReady=true */,
WINDOW_0 /* setPlayWhenReady=false */,
WINDOW_0 /* BUFFERING */,
contentBeforeMidroll /* READY */,
contentAfterMidroll /* BUFFERING */,
@ -1367,7 +1389,7 @@ public final class DefaultAnalyticsCollectorTest {
contentAfterMidroll /* ENDED */)
.inOrder();
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(
contentAfterMidroll /* seek */,
@ -1435,21 +1457,17 @@ public final class DefaultAnalyticsCollectorTest {
@Test
public void notifyExternalEvents() throws Exception {
MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(ExoPlayer player) {
player.getAnalyticsCollector().notifySeekStarted();
}
})
.seek(/* positionMs= */ 0)
.play()
.build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_READY);
player.getAnalyticsCollector().notifySeekStarted();
player.seekTo(/* positionMs= */ 0);
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
@ -1460,7 +1478,14 @@ public final class DefaultAnalyticsCollectorTest {
public void drmEvents_singlePeriod() throws Exception {
MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1);
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
@ -1488,18 +1513,21 @@ public final class DefaultAnalyticsCollectorTest {
SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1),
new FakeMediaSource(
SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1));
TestAnalyticsListener listener =
runAnalyticsTest(
mediaSource,
// Wait for the media to be fully buffered before unblocking the DRM key request. This
// ensures both periods report the same load event (because period1's DRM session is
// already preacquired by the time the key load completes).
new ActionSchedule.Builder(TAG)
.waitForIsLoading(false)
.waitForIsLoading(true)
.waitForIsLoading(false)
.executeRunnable(mediaDrmCallback.keyCondition::open)
.build());
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
// Wait for the media to be fully buffered before unblocking the DRM key request. This
// ensures both periods report the same load event (because period1's DRM session is
// already preacquired by the time the key load completes).
runUntilIsLoading(player, /* expectedIsLoading= */ false);
runUntilIsLoading(player, /* expectedIsLoading= */ true);
runUntilIsLoading(player, /* expectedIsLoading= */ false);
mediaDrmCallback.keyCondition.open();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
@ -1525,7 +1553,14 @@ public final class DefaultAnalyticsCollectorTest {
SINGLE_PERIOD_TIMELINE,
drmSessionManager,
VIDEO_FORMAT_DRM_1.buildUpon().setDrmInitData(DRM_DATA_2).build()));
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilPlaybackState(player, Player.STATE_ENDED);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
@ -1552,13 +1587,16 @@ public final class DefaultAnalyticsCollectorTest {
.build(mediaDrmCallback);
MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1);
TestAnalyticsListener listener =
runAnalyticsTest(
mediaSource,
new ActionSchedule.Builder(TAG)
.waitForIsLoading(false)
.executeRunnable(mediaDrmCallback.keyCondition::open)
.build());
ExoPlayer player = setupPlayer();
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
player.play();
player.setMediaSource(mediaSource);
player.prepare();
runUntilIsLoading(player, /* expectedIsLoading= */ false);
mediaDrmCallback.keyCondition.open();
runUntilError(player);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0);
@ -1588,12 +1626,14 @@ public final class DefaultAnalyticsCollectorTest {
}
}
};
ExoPlayer player = setupPlayer(renderersFactory);
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
TestAnalyticsListener listener =
runAnalyticsTest(
new ConcatenatingMediaSource(source0, source1),
/* actionSchedule= */ null,
renderersFactory);
player.play();
player.setMediaSource(new ConcatenatingMediaSource(source0, source1));
player.prepare();
runUntilError(player);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
@ -1622,12 +1662,14 @@ public final class DefaultAnalyticsCollectorTest {
}
}
};
ExoPlayer player = setupPlayer(renderersFactory);
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
TestAnalyticsListener listener =
runAnalyticsTest(
new ConcatenatingMediaSource(source0, source1),
/* actionSchedule= */ null,
renderersFactory);
player.play();
player.setMediaSource(new ConcatenatingMediaSource(source0, source1));
player.prepare();
runUntilError(player);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
@ -1660,12 +1702,14 @@ public final class DefaultAnalyticsCollectorTest {
}
}
};
ExoPlayer player = setupPlayer(renderersFactory);
TestAnalyticsListener listener = new TestAnalyticsListener();
player.addAnalyticsListener(listener);
TestAnalyticsListener listener =
runAnalyticsTest(
new ConcatenatingMediaSource(source, source),
/* actionSchedule= */ null,
renderersFactory);
player.play();
player.setMediaSource(new ConcatenatingMediaSource(source, source));
player.prepare();
runUntilError(player);
populateEventIds(listener.lastReportedTimeline);
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
@ -1673,11 +1717,7 @@ public final class DefaultAnalyticsCollectorTest {
@Test
public void onEvents_isReportedWithCorrectEventTimes() throws Exception {
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
player.setVideoSurface(surface);
ExoPlayer player = setupPlayer();
AnalyticsListener listener = mock(AnalyticsListener.class);
Format[] formats =
new Format[] {
@ -1690,20 +1730,18 @@ public final class DefaultAnalyticsCollectorTest {
player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formats));
player.seekTo(2_000);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
ShadowLooper.runMainLooperToNextTask();
runMainLooperToNextTask();
// Move to another item and fail with a third one to trigger events with different EventTimes.
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
runUntilPlaybackState(player, Player.STATE_READY);
player.addMediaSource(new FakeMediaSource(new FakeTimeline(), formats));
player.play();
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilError(player);
ShadowLooper.runMainLooperToNextTask();
runUntilError(player);
runMainLooperToNextTask();
player.release();
surface.release();
// Verify that expected individual callbacks have been called and capture EventTimes.
ArgumentCaptor<AnalyticsListener.EventTime> individualTimelineChangedEventTimes =
@ -1928,48 +1966,6 @@ public final class DefaultAnalyticsCollectorTest {
.inOrder();
}
private void populateEventIds(Timeline timeline) {
period0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
period0Seq0 = period0;
period0Seq1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
window1Period0Seq1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
if (timeline.getPeriodCount() > 1) {
period1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
period1Seq1 = period1;
period1Seq0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
period1Seq2 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2));
window0Period1Seq0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
}
}
@Test
public void recursiveListenerInvocation_arrivesInCorrectOrder() {
AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT);
@ -2027,13 +2023,12 @@ public final class DefaultAnalyticsCollectorTest {
exoPlayer.setMediaSource(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
exoPlayer.prepare();
TestPlayerRunHelper.runUntilPlaybackState(exoPlayer, Player.STATE_READY);
runUntilPlaybackState(exoPlayer, Player.STATE_READY);
// Release and add delay on releasing thread to verify timestamps of events.
exoPlayer.release();
long releaseTimeMs = fakeClock.currentTimeMillis();
fakeClock.advanceTime(1);
ShadowLooper.idleMainLooper();
idleMainLooper();
// Verify video disable events and release events arrived in order.
ArgumentCaptor<AnalyticsListener.EventTime> videoDisabledEventTime =
@ -2059,49 +2054,79 @@ public final class DefaultAnalyticsCollectorTest {
assertThat(releasedEventTime.getValue().realtimeMs).isGreaterThan(videoDisableTimeMs);
}
private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception {
return runAnalyticsTest(mediaSource, /* actionSchedule= */ null);
private void populateEventIds(Timeline timeline) {
period0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
period0Seq0 = period0;
period0Seq1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
window1Period0Seq1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
if (timeline.getPeriodCount() > 1) {
period1 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
period1Seq1 = period1;
period1Seq0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
period1Seq2 =
new EventWindowAndPeriodId(
/* windowIndex= */ 1,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2));
window0Period1Seq0 =
new EventWindowAndPeriodId(
/* windowIndex= */ 0,
new MediaPeriodId(
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
}
}
private static TestAnalyticsListener runAnalyticsTest(
MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception {
RenderersFactory renderersFactory =
(eventHandler,
private static ExoPlayer setupPlayer() {
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
return setupPlayer(
/* renderersFactory= */ (eventHandler,
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) ->
new Renderer[] {
new FakeVideoRenderer(eventHandler, videoRendererEventListener),
new FakeAudioRenderer(eventHandler, audioRendererEventListener)
};
return runAnalyticsTest(mediaSource, actionSchedule, renderersFactory);
metadataRendererOutput) -> {
HandlerWrapper clockAwareHandler =
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
return new Renderer[] {
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
},
clock);
}
private static TestAnalyticsListener runAnalyticsTest(
MediaSource mediaSource,
@Nullable ActionSchedule actionSchedule,
RenderersFactory renderersFactory)
throws Exception {
private static ExoPlayer setupPlayer(RenderersFactory renderersFactory) {
return setupPlayer(renderersFactory, new FakeClock(/* isAutoAdvancing= */ true));
}
private static ExoPlayer setupPlayer(RenderersFactory renderersFactory, Clock clock) {
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
TestAnalyticsListener listener = new TestAnalyticsListener();
try {
new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext())
.setMediaSources(mediaSource)
.setRenderersFactory(renderersFactory)
.setVideoSurface(surface)
.setAnalyticsListener(listener)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
} catch (ExoPlaybackException e) {
// Ignore ExoPlaybackException as these may be expected.
} finally {
surface.release();
}
return listener;
ExoPlayer player =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setClock(clock)
.setRenderersFactory(renderersFactory)
.build();
player.setVideoSurface(surface);
return player;
}
private static final class EventWindowAndPeriodId {

View File

@ -16,10 +16,10 @@
package androidx.media3.test.utils;
import android.os.Handler;
import android.os.SystemClock;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException;
@ -29,13 +29,15 @@ import androidx.media3.exoplayer.audio.AudioRendererEventListener;
@UnstableApi
public class FakeAudioRenderer extends FakeRenderer {
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
private final HandlerWrapper handler;
private final AudioRendererEventListener eventListener;
private final DecoderCounters decoderCounters;
private boolean notifiedPositionAdvancing;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
public FakeAudioRenderer(HandlerWrapper handler, AudioRendererEventListener eventListener) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
this.handler = handler;
this.eventListener = eventListener;
decoderCounters = new DecoderCounters();
}
@ -43,30 +45,33 @@ public class FakeAudioRenderer extends FakeRenderer {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
handler.post(() -> eventListener.onAudioEnabled(decoderCounters));
notifiedPositionAdvancing = false;
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
handler.post(() -> eventListener.onAudioDisabled(decoderCounters));
}
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.audio.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
handler.post(
() -> eventListener.onAudioInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
handler.post(
() ->
eventListener.onAudioDecoderInitialized(
/* decoderName= */ "fake.audio.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0));
}
@Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (shouldProcess && !notifiedPositionAdvancing) {
eventDispatcher.positionAdvancing(System.currentTimeMillis());
handler.post(() -> eventListener.onAudioPositionAdvancing(System.currentTimeMillis()));
notifiedPositionAdvancing = true;
}
return shouldProcess;

View File

@ -16,13 +16,13 @@
package androidx.media3.test.utils;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException;
@ -34,7 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@UnstableApi
public class FakeVideoRenderer extends FakeRenderer {
private final VideoRendererEventListener.EventDispatcher eventDispatcher;
private final HandlerWrapper handler;
private final VideoRendererEventListener eventListener;
private final DecoderCounters decoderCounters;
private @MonotonicNonNull Format format;
@Nullable private Object output;
@ -43,9 +44,10 @@ public class FakeVideoRenderer extends FakeRenderer {
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
public FakeVideoRenderer(HandlerWrapper handler, VideoRendererEventListener eventListener) {
super(C.TRACK_TYPE_VIDEO);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
this.handler = handler;
this.eventListener = eventListener;
decoderCounters = new DecoderCounters();
}
@ -53,7 +55,7 @@ public class FakeVideoRenderer extends FakeRenderer {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters);
handler.post(() -> eventListener.onVideoEnabled(decoderCounters));
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
@ -69,15 +71,17 @@ public class FakeVideoRenderer extends FakeRenderer {
@Override
protected void onStopped() {
super.onStopped();
eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0);
eventDispatcher.reportVideoFrameProcessingOffset(
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10);
handler.post(() -> eventListener.onDroppedFrames(/* count= */ 0, /* elapsedMs= */ 0));
handler.post(
() ->
eventListener.onVideoFrameProcessingOffset(
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10));
}
@Override
protected void onDisabled() {
super.onDisabled();
eventDispatcher.disabled(decoderCounters);
handler.post(() -> eventListener.onVideoDisabled(decoderCounters));
}
@Override
@ -88,11 +92,14 @@ public class FakeVideoRenderer extends FakeRenderer {
@Override
protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null);
eventDispatcher.decoderInitialized(
/* decoderName= */ "fake.video.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0);
handler.post(
() -> eventListener.onVideoInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
handler.post(
() ->
eventListener.onVideoDecoderInitialized(
/* decoderName= */ "fake.video.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0));
this.format = format;
}
@ -133,10 +140,18 @@ public class FakeVideoRenderer extends FakeRenderer {
@Nullable Object output = this.output;
if (shouldProcess && !renderedFirstFrameAfterReset && output != null) {
@MonotonicNonNull Format format = Assertions.checkNotNull(this.format);
eventDispatcher.videoSizeChanged(
new VideoSize(
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio));
eventDispatcher.renderedFirstFrame(output);
handler.post(
() ->
eventListener.onVideoSizeChanged(
new VideoSize(
format.width,
format.height,
format.rotationDegrees,
format.pixelWidthHeightRatio)));
handler.post(
() ->
eventListener.onRenderedFirstFrame(
output, /* renderTimeMs= */ SystemClock.elapsedRealtime()));
renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterEnable = true;
}

View File

@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
@ -299,13 +300,16 @@ public class TestExoPlayerBuilder {
videoRendererEventListener,
audioRendererEventListener,
textRendererOutput,
metadataRendererOutput) ->
renderers != null
? renderers
: new Renderer[] {
new FakeVideoRenderer(eventHandler, videoRendererEventListener),
new FakeAudioRenderer(eventHandler, audioRendererEventListener)
};
metadataRendererOutput) -> {
HandlerWrapper clockAwareHandler =
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
return renderers != null
? renderers
: new Renderer[] {
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
};
}
ExoPlayer.Builder builder =

View File

@ -91,6 +91,30 @@ public class TestPlayerRunHelper {
}
}
/**
* Runs tasks of the main {@link Looper} until {@link Player#isLoading()} 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 expectedIsLoading The expected value for {@link Player#isLoading()}.
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
* exceeded.
*/
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());
}
}
/**
* Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
* expected timeline or a playback error occurs.