Consider shuffle order in Timeline.equals()
Previously two timelines that differed only in shuffle order were considered equal, which resulted in no call to Player.Listener.onTimelineChanged when calling ExoPlayer.setShuffleOrder. This in turn resulted in no call to MediaControllerCompat.Callback.onQueueChanged. Also make a small fix inside ExoPlayerImpl.setShuffleOrder, to ensure that the new shuffle order is used when constructing the masked timeline. Issue: google/ExoPlayer#9889 #minor-release PiperOrigin-RevId: 457703727
This commit is contained in:
parent
2a2d9e360b
commit
6f9ce4056c
@ -2,6 +2,11 @@
|
||||
|
||||
### Unreleased changes
|
||||
|
||||
* Core library:
|
||||
* Ensure that changing the `ShuffleOrder` with `ExoPlayer.setShuffleOrder`
|
||||
results in a call to `Player.Listener#onTimelineChanged` with
|
||||
`reason=Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED`
|
||||
([#9889](https://github.com/google/ExoPlayer/issues/9889)).
|
||||
* Extractors:
|
||||
* Add support for AVI
|
||||
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
|
||||
|
@ -75,6 +75,7 @@ dependencies {
|
||||
testImplementation 'junit:junit:' + junitVersion
|
||||
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'lib-exoplayer')
|
||||
testImplementation project(modulePrefix + 'test-utils')
|
||||
}
|
||||
|
||||
|
@ -1351,6 +1351,27 @@ public abstract class Timeline implements Bundleable {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check shuffled order
|
||||
int windowIndex = getFirstWindowIndex(/* shuffleModeEnabled= */ true);
|
||||
if (windowIndex != other.getFirstWindowIndex(/* shuffleModeEnabled= */ true)) {
|
||||
return false;
|
||||
}
|
||||
int lastWindowIndex = getLastWindowIndex(/* shuffleModeEnabled= */ true);
|
||||
if (lastWindowIndex != other.getLastWindowIndex(/* shuffleModeEnabled= */ true)) {
|
||||
return false;
|
||||
}
|
||||
while (windowIndex != lastWindowIndex) {
|
||||
int nextWindowIndex =
|
||||
getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true);
|
||||
if (nextWindowIndex
|
||||
!= other.getNextWindowIndex(
|
||||
windowIndex, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
|
||||
return false;
|
||||
}
|
||||
windowIndex = nextWindowIndex;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1367,6 +1388,13 @@ public abstract class Timeline implements Bundleable {
|
||||
for (int i = 0; i < getPeriodCount(); i++) {
|
||||
result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode();
|
||||
}
|
||||
|
||||
for (int windowIndex = getFirstWindowIndex(true);
|
||||
windowIndex != C.INDEX_UNSET;
|
||||
windowIndex = getNextWindowIndex(windowIndex, Player.REPEAT_MODE_OFF, true)) {
|
||||
result = 31 * result + windowIndex;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.MediaItem.LiveConfiguration;
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
import androidx.media3.test.utils.FakeTimeline;
|
||||
import androidx.media3.test.utils.FakeTimeline.TimelineWindowDefinition;
|
||||
import androidx.media3.test.utils.TimelineAsserts;
|
||||
@ -64,6 +65,50 @@ public class TimelineTest {
|
||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timelineEquals() {
|
||||
ImmutableList<TimelineWindowDefinition> timelineWindowDefinitions =
|
||||
ImmutableList.of(
|
||||
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
|
||||
new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
|
||||
new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
|
||||
Timeline timeline1 =
|
||||
new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
|
||||
Timeline timeline2 =
|
||||
new FakeTimeline(timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
|
||||
|
||||
assertThat(timeline1).isEqualTo(timeline2);
|
||||
assertThat(timeline1.hashCode()).isEqualTo(timeline2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timelineEquals_includesShuffleOrder() {
|
||||
ImmutableList<TimelineWindowDefinition> timelineWindowDefinitions =
|
||||
ImmutableList.of(
|
||||
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 111),
|
||||
new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 222),
|
||||
new TimelineWindowDefinition(/* periodCount= */ 3, /* id= */ 333));
|
||||
Timeline timeline =
|
||||
new FakeTimeline(
|
||||
new Object[0],
|
||||
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
|
||||
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
|
||||
Timeline timelineWithEquivalentShuffleOrder =
|
||||
new FakeTimeline(
|
||||
new Object[0],
|
||||
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 5),
|
||||
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
|
||||
Timeline timelineWithDifferentShuffleOrder =
|
||||
new FakeTimeline(
|
||||
new Object[0],
|
||||
new DefaultShuffleOrder(timelineWindowDefinitions.size(), /* randomSeed= */ 3),
|
||||
timelineWindowDefinitions.toArray(new TimelineWindowDefinition[0]));
|
||||
|
||||
assertThat(timeline).isEqualTo(timelineWithEquivalentShuffleOrder);
|
||||
assertThat(timeline.hashCode()).isEqualTo(timelineWithEquivalentShuffleOrder.hashCode());
|
||||
assertThat(timeline).isNotEqualTo(timelineWithDifferentShuffleOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void windowEquals() {
|
||||
MediaItem mediaItem = new MediaItem.Builder().setUri("uri").setTag(new Object()).build();
|
||||
|
@ -710,6 +710,7 @@ import java.util.concurrent.TimeoutException;
|
||||
@Override
|
||||
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||
verifyApplicationThread();
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
Timeline timeline = createMaskingTimeline();
|
||||
PlaybackInfo newPlaybackInfo =
|
||||
maskTimelineAndPosition(
|
||||
@ -718,7 +719,6 @@ import java.util.concurrent.TimeoutException;
|
||||
maskWindowPositionMsOrGetPeriodPositionUs(
|
||||
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
|
||||
pendingOperationAcks++;
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
internalPlayer.setShuffleOrder(shuffleOrder);
|
||||
updatePlaybackInfo(
|
||||
newPlaybackInfo,
|
||||
|
@ -125,6 +125,7 @@ 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.ShuffleOrder;
|
||||
import androidx.media3.exoplayer.source.SinglePeriodTimeline;
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||
import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionMediaSource;
|
||||
@ -6517,6 +6518,53 @@ public final class ExoPlayerTest {
|
||||
assertThat(positionAfterSetShuffleOrder.get()).isAtLeast(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setShuffleOrder_notifiesTimelineChanged() throws Exception {
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||
.build();
|
||||
// No callback expected for this call, because the (empty) timeline doesn't change. We start
|
||||
// with a deterministic shuffle order, to ensure when we call setShuffleOrder again below the
|
||||
// order is definitely different (otherwise the test is flaky when the existing shuffle order
|
||||
// matches the shuffle order passed in below).
|
||||
player.setShuffleOrder(new FakeShuffleOrder(0));
|
||||
player.setMediaSources(
|
||||
ImmutableList.of(new FakeMediaSource(), new FakeMediaSource(), new FakeMediaSource()));
|
||||
Player.Listener mockListener = mock(Player.Listener.class);
|
||||
player.addListener(mockListener);
|
||||
player.prepare();
|
||||
TestPlayerRunHelper.playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 5000);
|
||||
player.play();
|
||||
ShuffleOrder.DefaultShuffleOrder newShuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(player.getMediaItemCount(), /* randomSeed= */ 5);
|
||||
player.setShuffleOrder(newShuffleOrder);
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
player.release();
|
||||
|
||||
ArgumentCaptor<Timeline> timelineCaptor = ArgumentCaptor.forClass(Timeline.class);
|
||||
verify(mockListener)
|
||||
.onTimelineChanged(
|
||||
timelineCaptor.capture(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||
|
||||
Timeline capturedTimeline = Iterables.getOnlyElement(timelineCaptor.getAllValues());
|
||||
List<Integer> newShuffleOrderIndexes = new ArrayList<>(newShuffleOrder.getLength());
|
||||
for (int i = newShuffleOrder.getFirstIndex();
|
||||
i != C.INDEX_UNSET;
|
||||
i = newShuffleOrder.getNextIndex(i)) {
|
||||
newShuffleOrderIndexes.add(i);
|
||||
}
|
||||
List<Integer> capturedTimelineShuffleIndexes = new ArrayList<>(newShuffleOrder.getLength());
|
||||
for (int i = capturedTimeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true);
|
||||
i != C.INDEX_UNSET;
|
||||
i =
|
||||
capturedTimeline.getNextWindowIndex(
|
||||
i, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true)) {
|
||||
capturedTimelineShuffleIndexes.add(i);
|
||||
}
|
||||
assertThat(capturedTimelineShuffleIndexes).isEqualTo(newShuffleOrderIndexes);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMediaSources_empty_whenEmpty_correctMaskingMediaItemIndex() throws Exception {
|
||||
final int[] currentMediaItemIndices = {C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET};
|
||||
|
@ -29,6 +29,7 @@ import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.ArrayList;
|
||||
@ -275,7 +276,7 @@ public final class FakeTimeline extends Timeline {
|
||||
private final TimelineWindowDefinition[] windowDefinitions;
|
||||
private final Object[] manifests;
|
||||
private final int[] periodOffsets;
|
||||
private final FakeShuffleOrder fakeShuffleOrder;
|
||||
private final ShuffleOrder shuffleOrder;
|
||||
|
||||
/**
|
||||
* Returns an ad playback state with the specified number of ads in each of the specified ad
|
||||
@ -395,6 +396,19 @@ public final class FakeTimeline extends Timeline {
|
||||
* @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
|
||||
*/
|
||||
public FakeTimeline(Object[] manifests, TimelineWindowDefinition... windowDefinitions) {
|
||||
this(manifests, new FakeShuffleOrder(windowDefinitions.length), windowDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fake timeline with the given window definitions and {@link
|
||||
* androidx.media3.exoplayer.source.ShuffleOrder}.
|
||||
*
|
||||
* @param windowDefinitions A list of {@link TimelineWindowDefinition}s.
|
||||
*/
|
||||
public FakeTimeline(
|
||||
Object[] manifests,
|
||||
ShuffleOrder shuffleOrder,
|
||||
TimelineWindowDefinition... windowDefinitions) {
|
||||
this.manifests = new Object[windowDefinitions.length];
|
||||
System.arraycopy(manifests, 0, this.manifests, 0, min(this.manifests.length, manifests.length));
|
||||
this.windowDefinitions = windowDefinitions;
|
||||
@ -403,7 +417,7 @@ public final class FakeTimeline extends Timeline {
|
||||
for (int i = 0; i < windowDefinitions.length; i++) {
|
||||
periodOffsets[i + 1] = periodOffsets[i] + windowDefinitions[i].periodCount;
|
||||
}
|
||||
fakeShuffleOrder = new FakeShuffleOrder(windowDefinitions.length);
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -422,7 +436,7 @@ public final class FakeTimeline extends Timeline {
|
||||
? getFirstWindowIndex(shuffleModeEnabled)
|
||||
: C.INDEX_UNSET;
|
||||
}
|
||||
return shuffleModeEnabled ? fakeShuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
|
||||
return shuffleModeEnabled ? shuffleOrder.getNextIndex(windowIndex) : windowIndex + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -436,20 +450,20 @@ public final class FakeTimeline extends Timeline {
|
||||
? getLastWindowIndex(shuffleModeEnabled)
|
||||
: C.INDEX_UNSET;
|
||||
}
|
||||
return shuffleModeEnabled ? fakeShuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
|
||||
return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(windowIndex) : windowIndex - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastWindowIndex(boolean shuffleModeEnabled) {
|
||||
return shuffleModeEnabled
|
||||
? fakeShuffleOrder.getLastIndex()
|
||||
? shuffleOrder.getLastIndex()
|
||||
: super.getLastWindowIndex(/* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
|
||||
return shuffleModeEnabled
|
||||
? fakeShuffleOrder.getFirstIndex()
|
||||
? shuffleOrder.getFirstIndex()
|
||||
: super.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
|
@ -200,8 +200,12 @@ public class TestUtil {
|
||||
|
||||
/**
|
||||
* Asserts that the actual timelines are the same to the expected timelines. This assert differs
|
||||
* from testing equality by not comparing period ids which may be different due to id mapping of
|
||||
* child source period ids.
|
||||
* from testing equality by not comparing:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Period IDs, which may be different due to ID mapping of child source period IDs.
|
||||
* <li>Shuffle order, which by default is random and non-deterministic.
|
||||
* </ul>
|
||||
*
|
||||
* @param actualTimelines A list of actual {@link Timeline timelines}.
|
||||
* @param expectedTimelines A list of expected {@link Timeline timelines}.
|
||||
@ -218,10 +222,11 @@ public class TestUtil {
|
||||
|
||||
/**
|
||||
* Returns true if {@code thisTimeline} is equal to {@code thatTimeline}, ignoring {@link
|
||||
* Timeline.Window#uid} and {@link Timeline.Period#uid} values.
|
||||
* Timeline.Window#uid} and {@link Timeline.Period#uid} values, and shuffle order.
|
||||
*/
|
||||
public static boolean timelinesAreSame(Timeline thisTimeline, Timeline thatTimeline) {
|
||||
return new NoUidTimeline(thisTimeline).equals(new NoUidTimeline(thatTimeline));
|
||||
return new NoUidOrShufflingTimeline(thisTimeline)
|
||||
.equals(new NoUidOrShufflingTimeline(thatTimeline));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -505,11 +510,11 @@ public class TestUtil {
|
||||
return list;
|
||||
}
|
||||
|
||||
private static final class NoUidTimeline extends Timeline {
|
||||
private static final class NoUidOrShufflingTimeline extends Timeline {
|
||||
|
||||
private final Timeline delegate;
|
||||
|
||||
public NoUidTimeline(Timeline timeline) {
|
||||
public NoUidOrShufflingTimeline(Timeline timeline) {
|
||||
this.delegate = timeline;
|
||||
}
|
||||
|
||||
@ -518,6 +523,27 @@ public class TestUtil {
|
||||
return delegate.getWindowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
|
||||
return delegate.getNextWindowIndex(windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex(int windowIndex, int repeatMode, boolean shuffleModeEnabled) {
|
||||
return delegate.getPreviousWindowIndex(
|
||||
windowIndex, repeatMode, /* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastWindowIndex(boolean shuffleModeEnabled) {
|
||||
return delegate.getLastWindowIndex(/* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
|
||||
return delegate.getFirstWindowIndex(/* shuffleModeEnabled= */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
|
||||
delegate.getWindow(windowIndex, window, defaultPositionProjectionUs);
|
||||
|
Loading…
x
Reference in New Issue
Block a user