diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d77693b562a..c1522a7e656 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,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 `audioConversionProcess` and `videoConversionProcess` to `ExportResult` indicating how the respective track in the output file 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 f010a966064..6628bcf6a03 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 @@ private void reselectTracksInternal() throws ExoPlaybackException { 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 @@ private void reselectTracksInternal() throws ExoPlaybackException { 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 d8c9d610da4..7999271b33c 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.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.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.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 void allActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreReused() 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 PlaybackParameters getPlaybackParameters() { } } + /** 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. */