From 459162c692197c9d577cc601839af22eb719cc35 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Mon, 9 Dec 2024 10:08:45 -0800 Subject: [PATCH] Implement seeking support for pre-warming renderer feature PiperOrigin-RevId: 704328162 --- .../ExoPlayerWithPrewarmingRenderersTest.java | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java index 3d72975575..e4b5dcf9bc 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerWithPrewarmingRenderersTest.java @@ -15,22 +15,38 @@ */ package androidx.media3.exoplayer; +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.run; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.os.Handler; +import androidx.annotation.Nullable; +import androidx.media3.common.C; +import androidx.media3.common.Format; import androidx.media3.common.Player; +import androidx.media3.common.Timeline; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; +import androidx.media3.datasource.TransferListener; +import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.audio.AudioRendererEventListener; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.metadata.MetadataOutput; +import androidx.media3.exoplayer.source.MediaPeriod; +import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.test.utils.ExoPlayerTestRunner; import androidx.media3.test.utils.FakeAudioRenderer; import androidx.media3.test.utils.FakeClock; +import androidx.media3.test.utils.FakeMediaPeriod; import androidx.media3.test.utils.FakeMediaSource; +import androidx.media3.test.utils.FakeSampleStream; import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeVideoRenderer; import androidx.media3.test.utils.TestExoPlayerBuilder; @@ -38,6 +54,7 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -289,6 +306,198 @@ public class ExoPlayerWithPrewarmingRenderersTest { assertThat(videoState3).isEqualTo(Renderer.STATE_ENABLED); } + @Test + public void + seek_intoCurrentPeriodWithSecondaryBeforeReadingPeriodAdvanced_doesNotSwapToPrimaryRenderer() + throws Exception { + Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock(fakeClock) + .setRenderersFactory( + new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock)) + .build(); + Renderer videoRenderer = player.getRenderer(/* index= */ 0); + Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0); + // Set a playlist that allows a new renderer to be enabled early. + player.setMediaSources( + ImmutableList.of( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + // Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming. + new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource( + new FakeTimeline(), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT))); + player.prepare(); + + // Play a bit until the second renderer is started. + run(player).untilStartOfMediaItem(/* mediaItemIndex= */ 1); + run(player).untilPendingCommandsAreFullyHandled(); + @Renderer.State int videoState1 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState(); + // Seek to position in current period. + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + run(player).untilPendingCommandsAreFullyHandled(); + @Renderer.State int videoState2 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState(); + player.release(); + + assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED); + assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_STARTED); + assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED); + assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED); + } + + @Test + public void seek_intoCurrentPeriodWithSecondaryAndReadingPeriodAdvanced_swapsToPrimaryRenderer() + throws Exception { + Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock(fakeClock) + .setRenderersFactory( + new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock)) + .build(); + Renderer videoRenderer = player.getRenderer(/* index= */ 0); + Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0); + // Set a playlist that allows a new renderer to be enabled early. + player.setMediaSources( + ImmutableList.of( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource( + new FakeTimeline(), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT))); + player.prepare(); + + // Play a bit until the second renderer is started. + player.play(); + run(player) + .untilBackgroundThreadCondition( + () -> secondaryVideoRenderer.getState() == Renderer.STATE_STARTED); + run(player) + .untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_ENABLED); + @Renderer.State int videoState1 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState(); + // Seek to position in current period. + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 500); + run(player).untilPendingCommandsAreFullyHandled(); + // Play until secondary renderer is being pre-warmed on third media item. + run(player) + .untilBackgroundThreadCondition( + () -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED); + @Renderer.State int videoState2 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState(); + player.release(); + + assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED); + assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_STARTED); + assertThat(videoState2).isEqualTo(Renderer.STATE_STARTED); + assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_ENABLED); + } + + @Test + public void seek_pastReadingPeriodWithSecondaryRendererOnPlayingPeriod_swapsToPrimaryRenderer() + throws Exception { + Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + ExoPlayer player = + new TestExoPlayerBuilder(context) + .setClock(fakeClock) + .setRenderersFactory( + new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock)) + .build(); + Renderer videoRenderer = player.getRenderer(/* index= */ 0); + Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0); + // Set a playlist that allows a new renderer to be enabled early. + player.setMediaSources( + ImmutableList.of( + new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + // Use FakeBlockingMediaSource so that reading period is not advanced when pre-warming. + new FakeBlockingMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT), + new FakeMediaSource( + new FakeTimeline(), + ExoPlayerTestRunner.VIDEO_FORMAT, + ExoPlayerTestRunner.AUDIO_FORMAT))); + player.prepare(); + + // Play a bit until the second renderer is started. + run(player).untilStartOfMediaItem(/* mediaItemIndex= */ 1); + run(player).untilPendingCommandsAreFullyHandled(); + @Renderer.State int videoState1 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState(); + // Seek to position in following period. + player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 3000); + run(player).untilPendingCommandsAreFullyHandled(); + @Renderer.State int videoState2 = videoRenderer.getState(); + @Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState(); + player.release(); + + assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED); + assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_STARTED); + assertThat(videoState2).isEqualTo(Renderer.STATE_STARTED); + assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_DISABLED); + } + + /** {@link FakeMediaSource} that prevents any reading of samples off the sample queue. */ + private static final class FakeBlockingMediaSource extends FakeMediaSource { + + public FakeBlockingMediaSource(Timeline timeline, Format format) { + super(timeline, format); + } + + @Override + protected MediaPeriod createMediaPeriod( + MediaPeriodId id, + TrackGroupArray trackGroupArray, + Allocator allocator, + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + @Nullable TransferListener transferListener) { + long startPositionUs = + -getTimeline() + .getPeriodByUid(id.periodUid, new Timeline.Period()) + .getPositionInWindowUs(); + return new FakeMediaPeriod( + trackGroupArray, + allocator, + (format, mediaPeriodId) -> + ImmutableList.of( + oneByteSample(startPositionUs, C.BUFFER_FLAG_KEY_FRAME), + oneByteSample(startPositionUs + 10_000), + END_OF_STREAM_ITEM), + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ false) { + @Override + protected FakeSampleStream createSampleStream( + Allocator allocator, + @Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + DrmSessionManager drmSessionManager, + DrmSessionEventListener.EventDispatcher drmEventDispatcher, + Format initialFormat, + List fakeSampleStreamItems) { + return new FakeSampleStream( + allocator, + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + initialFormat, + fakeSampleStreamItems) { + @Override + public int readData( + FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) { + return C.RESULT_NOTHING_READ; + } + }; + } + }; + } + } + private static class FakeRenderersFactorySupportingSecondaryVideoRenderer implements RenderersFactory { protected final Clock clock;