diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index d92a5e8b87..6c3c64075b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -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); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java index f812c11ed5..7649d1bbe8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaPeriodQueue.java @@ -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(); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java index 5bc6e1026a..21cd5ceec4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaSourceList.java @@ -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 mediaSourceByMediaPeriod; private final Map mediaSourceByUid; private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener; - private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher; - private final DrmSessionEventListener.EventDispatcher drmEventDispatcher; private final HashMap childSources; private final Set 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 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 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 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 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 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 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 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 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 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 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 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 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 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); } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 4afbf6aef9..67316a0178 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -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); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java index ca3bd02eeb..2ab4681030 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaPeriodQueueTest.java @@ -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); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java index 7a6b9a7c4d..8156a5c85b 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/MediaSourceListTest.java @@ -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); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index c25ddebdcf..ceefe172c7 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -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 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 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 { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java index 7a476d16b5..dc219d9b98 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAudioRenderer.java @@ -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; diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java index 9c9a4bb1b7..45de4ecad1 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeVideoRenderer.java @@ -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; } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java index f5e02695bf..afc58e3ef8 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java @@ -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 = diff --git a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java index 54d62208ef..173b6d2683 100644 --- a/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java +++ b/libraries/test_utils_robolectric/src/main/java/androidx/media3/test/utils/robolectric/TestPlayerRunHelper.java @@ -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. + * + *

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.