Implement seeking support for pre-warming renderer feature

PiperOrigin-RevId: 704328162
This commit is contained in:
michaelkatz 2024-12-09 10:08:45 -08:00 committed by Copybara-Service
parent 987869e456
commit 459162c692

View File

@ -15,22 +15,38 @@
*/ */
package androidx.media3.exoplayer; 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 androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.os.Handler; 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.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper; 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.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.metadata.MetadataOutput; 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.text.TextOutput;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.exoplayer.video.VideoRendererEventListener;
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;
import androidx.media3.test.utils.FakeMediaPeriod;
import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeSampleStream;
import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeVideoRenderer; import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.TestExoPlayerBuilder; 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.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -289,6 +306,198 @@ public class ExoPlayerWithPrewarmingRenderersTest {
assertThat(videoState3).isEqualTo(Renderer.STATE_ENABLED); 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<FakeSampleStream.FakeSampleStreamItem> 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 private static class FakeRenderersFactorySupportingSecondaryVideoRenderer
implements RenderersFactory { implements RenderersFactory {
protected final Clock clock; protected final Clock clock;