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; deliverPendingMessageAtStartPositionRequired = true;
Handler eventHandler = new Handler(applicationLooper); HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler); queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList = mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); 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.Player.RepeatMode;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; 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.Period period;
private final Timeline.Window window; private final Timeline.Window window;
private final AnalyticsCollector analyticsCollector; private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler; private final HandlerWrapper analyticsCollectorHandler;
private long nextWindowSequenceNumber; private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode; private @RepeatMode int repeatMode;
@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
* on. * on.
*/ */
public MediaPeriodQueue( public MediaPeriodQueue(
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) { AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector; this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler; this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period(); period = new Timeline.Period();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
@ -299,13 +300,16 @@ public class TestExoPlayerBuilder {
videoRendererEventListener, videoRendererEventListener,
audioRendererEventListener, audioRendererEventListener,
textRendererOutput, textRendererOutput,
metadataRendererOutput) -> metadataRendererOutput) -> {
renderers != null HandlerWrapper clockAwareHandler =
? renderers clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
: new Renderer[] { return renderers != null
new FakeVideoRenderer(eventHandler, videoRendererEventListener), ? renderers
new FakeAudioRenderer(eventHandler, audioRendererEventListener) : new Renderer[] {
}; new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
};
} }
ExoPlayer.Builder builder = 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 * Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
* expected timeline or a playback error occurs. * expected timeline or a playback error occurs.