mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Implement seeking support for pre-warming renderer feature
PiperOrigin-RevId: 704328162
This commit is contained in:
parent
987869e456
commit
459162c692
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user