From 81e91c25f129dd2b11797a11dc9d8f41f5cd9b12 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Thu, 22 Feb 2024 08:05:55 -0800 Subject: [PATCH] Use playing period TrackSelectorResult in track reselection update If the reading period has already advanced and a track reselection procs that only affects the reading period media, then ExoPlayer may try and apply the reading period's track selection incorrectly unto the playing period. ExoPlayer should apply the playing period's track selection to the playing period instead. PiperOrigin-RevId: 609375077 (cherry picked from commit 41929246222e9fdb9aa552db526c1b41d26bdb90) --- RELEASENOTES.md | 2 + .../exoplayer/ExoPlayerImplInternal.java | 10 +- .../media3/exoplayer/ExoPlayerTest.java | 181 ++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 09325057e2..96dcfccbe3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * ExoPlayer: * Fix issue where `PreloadMediaPeriod` cannot retain the streams when it is preloaded again. + * Apply the correct corresponding `TrackSelectionResult` to the playing + period in track reselection. * Transformer: * Add workaround for exception thrown due to `MediaMuxer` not supporting negative presentation timestamps before API 30. 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 f010a96606..6628bcf6a0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -1801,12 +1801,17 @@ import java.util.concurrent.atomic.AtomicBoolean; MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean selectionsChangedForReadPeriod = true; TrackSelectorResult newTrackSelectorResult; + // Keep playing period result in case of track selection change for reading period only. + TrackSelectorResult newPlayingPeriodTrackSelectorResult = null; while (true) { if (periodHolder == null || !periodHolder.prepared) { // The reselection did not change any prepared periods. return; } newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline); + if (periodHolder == queue.getPlayingPeriod()) { + newPlayingPeriodTrackSelectorResult = newTrackSelectorResult; + } if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) { // Selected tracks have changed for this period. break; @@ -1826,7 +1831,10 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.applyTrackSelection( - newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags); + checkNotNull(newPlayingPeriodTrackSelectorResult), + playbackInfo.positionUs, + recreateStreams, + streamResetFlags); boolean hasDiscontinuity = playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs; 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 d8c9d610da..7999271b33 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -119,6 +119,7 @@ import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.Listener; import androidx.media3.common.Player.PlayWhenReadyChangeReason; import androidx.media3.common.Player.PositionInfo; +import androidx.media3.common.StreamKey; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Window; import androidx.media3.common.TrackGroup; @@ -129,6 +130,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; +import androidx.media3.common.util.NullableType; import androidx.media3.common.util.SystemClock; import androidx.media3.common.util.Util; import androidx.media3.datasource.TransferListener; @@ -146,12 +148,15 @@ import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSourceEventListener; +import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.ShuffleOrder; import androidx.media3.exoplayer.source.SinglePeriodTimeline; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.source.WrappingMediaSource; import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource; import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.upstream.Allocation; import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.Loader; @@ -1410,6 +1415,88 @@ public final class ExoPlayerTest { assertThat(numSelectionsEnabled).isEqualTo(3); } + @Test + public void setTrackSelectionParameters_onlyAffectingReadingPeriodMediaItem_selectsCorrectTracks() + throws Exception { + Format audioFormat1 = + ExoPlayerTestRunner.AUDIO_FORMAT.buildUpon().setAverageBitrate(50_000).build(); + Format audioFormat2 = audioFormat1.buildUpon().setAverageBitrate(100_000).build(); + Format audioFormat3 = audioFormat1.buildUpon().setAverageBitrate(60_000).build(); + DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(context); + defaultTrackSelector.setParameters( + defaultTrackSelector + .buildUponParameters() + .setExceedAudioConstraintsIfNecessary(false) + .build()); + Timeline timeline = new FakeTimeline(); + AtomicInteger createMediaPeriodCounter = new AtomicInteger(); + AtomicReference exoTrackSelectionAtomicReferenceMediaItem1 = + new AtomicReference<>(); + AtomicReference exoTrackSelectionAtomicReferenceMediaItem2 = + new AtomicReference<>(); + ExoPlayer player = + new TestExoPlayerBuilder(context).setTrackSelector(defaultTrackSelector).build(); + player.addMediaSources( + ImmutableList.of( + new FakeMediaSource(timeline, audioFormat1) { + @Override + public MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + return new ForwardingMediaPeriod( + super.createPeriod(id, allocator, startPositionUs), + exoTrackSelectionAtomicReferenceMediaItem1); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (mediaPeriod instanceof ForwardingMediaPeriod) { + super.releasePeriod(((ForwardingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + super.releasePeriod(mediaPeriod); + } + } + }, + new FakeMediaSource(timeline, audioFormat2, audioFormat3) { + @Override + public MediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + createMediaPeriodCounter.getAndIncrement(); + return new ForwardingMediaPeriod( + super.createPeriod(id, allocator, startPositionUs), + exoTrackSelectionAtomicReferenceMediaItem2); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (mediaPeriod instanceof ForwardingMediaPeriod) { + super.releasePeriod(((ForwardingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + super.releasePeriod(mediaPeriod); + } + } + })); + player.prepare(); + + TestPlayerRunHelper.playUntilPosition( + player, /* mediaItemIndex= */ 0, /* positionMs= */ 5 * C.MICROS_PER_SECOND); + assertThat(exoTrackSelectionAtomicReferenceMediaItem2.get()[1]).isNotNull(); + assertThat(exoTrackSelectionAtomicReferenceMediaItem2.get()[1].getFormat(0)) + .isEqualTo(audioFormat2); + // Alter track selection parameters to invalidate track selection on second media item only. + player.setTrackSelectionParameters( + player.getTrackSelectionParameters().buildUpon().setMaxAudioBitrate(70_000).build()); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + assertThat(createMediaPeriodCounter.get()).isEqualTo(2); + assertThat(exoTrackSelectionAtomicReferenceMediaItem1.get()[1]).isNotNull(); + assertThat(exoTrackSelectionAtomicReferenceMediaItem1.get()[1].getFormat(0)) + .isEqualTo(audioFormat1); + assertThat(exoTrackSelectionAtomicReferenceMediaItem2.get()[1]).isNotNull(); + assertThat(exoTrackSelectionAtomicReferenceMediaItem2.get()[1].getFormat(0)) + .isEqualTo(audioFormat3); + } + @Test public void dynamicTimelineChangeReason() throws Exception { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); @@ -14563,6 +14650,100 @@ public final class ExoPlayerTest { } } + /** Forwarding {@link MediaPeriod} class with tracked reference for {@link #selectTracks}. */ + private static final class ForwardingMediaPeriod implements MediaPeriod { + + /** The wrapped {@link MediaPeriod} that method calls are forwarded to. */ + public final MediaPeriod mediaPeriod; + + /** Reference to last tracks selected through {@linkplain #selectTracks}. */ + public final AtomicReference exoTrackSelectionReferenceList; + + public ForwardingMediaPeriod( + MediaPeriod mediaPeriod, + AtomicReference exoTrackSelectionReferenceList) { + this.mediaPeriod = mediaPeriod; + this.exoTrackSelectionReferenceList = exoTrackSelectionReferenceList; + } + + @Override + public void prepare(Callback callback, long positionUs) { + mediaPeriod.prepare(callback, positionUs); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + mediaPeriod.maybeThrowPrepareError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public List getStreamKeys(List trackSelections) { + return mediaPeriod.getStreamKeys(trackSelections); + } + + @Override + public long selectTracks( + @NullableType ExoTrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { + exoTrackSelectionReferenceList.set(selections); + return mediaPeriod.selectTracks( + selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(LoadingInfo loadingInfo) { + return mediaPeriod.continueLoading(loadingInfo); + } + + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); + } + + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs); + } + } + /** * Returns an argument matcher for {@link Timeline} instances that ignores period and window uids. */