Avoid position jumping back when controller replaces the current item
When the controller replaces the current item, the masking position will be changed to the default position of the new item for a short while, before the correct position comes from the session. This will interrupt the current position fetched from the controller when the playback doesn't interrupted by the item replacing. Issue: androidx/media#951 #minor-release PiperOrigin-RevId: 611417539
This commit is contained in:
parent
27d78e69d7
commit
1bdc58de0b
@ -49,6 +49,9 @@
|
|||||||
* Muxers:
|
* Muxers:
|
||||||
* IMA extension:
|
* IMA extension:
|
||||||
* Session:
|
* Session:
|
||||||
|
* Fix issue where the current position jumps back when the controller
|
||||||
|
replaces the current item
|
||||||
|
([#951](https://github.com/androidx/media/issues/951)).
|
||||||
* UI:
|
* UI:
|
||||||
* Fallback to include audio track language name if `Locale` cannot
|
* Fallback to include audio track language name if `Locale` cannot
|
||||||
identify a display name
|
identify a display name
|
||||||
|
@ -970,7 +970,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
}
|
}
|
||||||
// Add media items to the end of the timeline if the index exceeds the window count.
|
// Add media items to the end of the timeline if the index exceeds the window count.
|
||||||
index = min(index, playerInfo.timeline.getWindowCount());
|
index = min(index, playerInfo.timeline.getWindowCount());
|
||||||
PlayerInfo newPlayerInfo = maskPlaybackInfoForAddedItems(playerInfo, index, mediaItems);
|
PlayerInfo newPlayerInfo =
|
||||||
|
maskPlayerInfoForAddedItems(
|
||||||
|
playerInfo, index, mediaItems, getCurrentPosition(), getContentPosition());
|
||||||
@Nullable
|
@Nullable
|
||||||
@Player.MediaItemTransitionReason
|
@Player.MediaItemTransitionReason
|
||||||
Integer mediaItemTransitionReason =
|
Integer mediaItemTransitionReason =
|
||||||
@ -983,8 +985,23 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
/* mediaItemTransitionReason= */ mediaItemTransitionReason);
|
/* mediaItemTransitionReason= */ mediaItemTransitionReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PlayerInfo maskPlaybackInfoForAddedItems(
|
/**
|
||||||
PlayerInfo playerInfo, int index, List<MediaItem> mediaItems) {
|
* Returns a masking {@link PlayerInfo} for the added {@linkplain MediaItem media items} with the
|
||||||
|
* provided information.
|
||||||
|
*
|
||||||
|
* @param playerInfo The {@link PlayerInfo} that the new masking {@link PlayerInfo} is based on.
|
||||||
|
* @param index The index at which the {@linkplain MediaItem media items} are added.
|
||||||
|
* @param mediaItems The {@linkplain MediaItem media items} added.
|
||||||
|
* @param currentPositionMs The current position in milliseconds.
|
||||||
|
* @param currentContentPositionMs The current content position in milliseconds.
|
||||||
|
* @return A masking {@link PlayerInfo}.
|
||||||
|
*/
|
||||||
|
private static PlayerInfo maskPlayerInfoForAddedItems(
|
||||||
|
PlayerInfo playerInfo,
|
||||||
|
int index,
|
||||||
|
List<MediaItem> mediaItems,
|
||||||
|
long currentPositionMs,
|
||||||
|
long currentContentPositionMs) {
|
||||||
Timeline oldTimeline = playerInfo.timeline;
|
Timeline oldTimeline = playerInfo.timeline;
|
||||||
List<Window> newWindows = new ArrayList<>();
|
List<Window> newWindows = new ArrayList<>();
|
||||||
List<Period> newPeriods = new ArrayList<>();
|
List<Period> newPeriods = new ArrayList<>();
|
||||||
@ -1017,6 +1034,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
newTimeline,
|
newTimeline,
|
||||||
newMediaItemIndex,
|
newMediaItemIndex,
|
||||||
newPeriodIndex,
|
newPeriodIndex,
|
||||||
|
currentPositionMs,
|
||||||
|
currentContentPositionMs,
|
||||||
Player.DISCONTINUITY_REASON_INTERNAL);
|
Player.DISCONTINUITY_REASON_INTERNAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1066,7 +1085,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
}
|
}
|
||||||
boolean wasCurrentItemRemoved =
|
boolean wasCurrentItemRemoved =
|
||||||
getCurrentMediaItemIndex() >= fromIndex && getCurrentMediaItemIndex() < toIndex;
|
getCurrentMediaItemIndex() >= fromIndex && getCurrentMediaItemIndex() < toIndex;
|
||||||
PlayerInfo newPlayerInfo = maskPlayerInfoForRemovedItems(playerInfo, fromIndex, toIndex);
|
PlayerInfo newPlayerInfo =
|
||||||
|
maskPlayerInfoForRemovedItems(
|
||||||
|
playerInfo,
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
/* isReplacingItems= */ false,
|
||||||
|
getCurrentPosition(),
|
||||||
|
getContentPosition());
|
||||||
boolean didMediaItemTransitionHappen =
|
boolean didMediaItemTransitionHappen =
|
||||||
playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex
|
playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex
|
||||||
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex;
|
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex;
|
||||||
@ -1082,8 +1108,30 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a masking {@link PlayerInfo} for the removed {@linkplain MediaItem media items} with
|
||||||
|
* the provided information.
|
||||||
|
*
|
||||||
|
* @param playerInfo The {@link PlayerInfo} that the new masking {@link PlayerInfo} is based on.
|
||||||
|
* @param fromIndex The index at which to start removing media items (inclusive).
|
||||||
|
* @param toIndex The index of the first item to be kept (exclusive).
|
||||||
|
* @param isReplacingItems A boolean indicating whether the media items are removed due to
|
||||||
|
* replacing.
|
||||||
|
* @param currentPositionMs The current position in milliseconds. This value will be used in the
|
||||||
|
* new masking {@link PlayerInfo} if the removal of the media items doesn't affect the current
|
||||||
|
* playback position.
|
||||||
|
* @param currentContentPositionMs The current content position in milliseconds. This value will
|
||||||
|
* be used in the new masking {@link PlayerInfo} if the removal of the media items doesn't
|
||||||
|
* affect the current playback position.
|
||||||
|
* @return A masking {@link PlayerInfo}.
|
||||||
|
*/
|
||||||
private static PlayerInfo maskPlayerInfoForRemovedItems(
|
private static PlayerInfo maskPlayerInfoForRemovedItems(
|
||||||
PlayerInfo playerInfo, int fromIndex, int toIndex) {
|
PlayerInfo playerInfo,
|
||||||
|
int fromIndex,
|
||||||
|
int toIndex,
|
||||||
|
boolean isReplacingItems,
|
||||||
|
long currentPositionMs,
|
||||||
|
long currentContentPositionMs) {
|
||||||
Timeline oldTimeline = playerInfo.timeline;
|
Timeline oldTimeline = playerInfo.timeline;
|
||||||
List<Window> newWindows = new ArrayList<>();
|
List<Window> newWindows = new ArrayList<>();
|
||||||
List<Period> newPeriods = new ArrayList<>();
|
List<Period> newPeriods = new ArrayList<>();
|
||||||
@ -1141,6 +1189,16 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
newPositionInfo,
|
newPositionInfo,
|
||||||
SessionPositionInfo.DEFAULT,
|
SessionPositionInfo.DEFAULT,
|
||||||
Player.DISCONTINUITY_REASON_REMOVE);
|
Player.DISCONTINUITY_REASON_REMOVE);
|
||||||
|
} else if (isReplacingItems) {
|
||||||
|
newPlayerInfo =
|
||||||
|
maskTimelineAndPositionInfo(
|
||||||
|
playerInfo,
|
||||||
|
newTimeline,
|
||||||
|
newMediaItemIndex,
|
||||||
|
newPeriodIndex,
|
||||||
|
currentPositionMs,
|
||||||
|
currentContentPositionMs,
|
||||||
|
Player.DISCONTINUITY_REASON_REMOVE);
|
||||||
} else {
|
} else {
|
||||||
Window newWindow = newTimeline.getWindow(newMediaItemIndex, new Window());
|
Window newWindow = newTimeline.getWindow(newMediaItemIndex, new Window());
|
||||||
long defaultPositionMs = newWindow.getDefaultPositionMs();
|
long defaultPositionMs = newWindow.getDefaultPositionMs();
|
||||||
@ -1182,6 +1240,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
newTimeline,
|
newTimeline,
|
||||||
newMediaItemIndex,
|
newMediaItemIndex,
|
||||||
newPeriodIndex,
|
newPeriodIndex,
|
||||||
|
currentPositionMs,
|
||||||
|
currentContentPositionMs,
|
||||||
Player.DISCONTINUITY_REASON_REMOVE);
|
Player.DISCONTINUITY_REASON_REMOVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1290,8 +1350,17 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toIndex = min(toIndex, playlistSize);
|
toIndex = min(toIndex, playlistSize);
|
||||||
PlayerInfo newPlayerInfo = maskPlaybackInfoForAddedItems(playerInfo, toIndex, mediaItems);
|
PlayerInfo newPlayerInfo =
|
||||||
newPlayerInfo = maskPlayerInfoForRemovedItems(newPlayerInfo, fromIndex, toIndex);
|
maskPlayerInfoForAddedItems(
|
||||||
|
playerInfo, toIndex, mediaItems, getCurrentPosition(), getContentPosition());
|
||||||
|
newPlayerInfo =
|
||||||
|
maskPlayerInfoForRemovedItems(
|
||||||
|
newPlayerInfo,
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
/* isReplacingItems= */ true,
|
||||||
|
getCurrentPosition(),
|
||||||
|
getContentPosition());
|
||||||
boolean wasCurrentItemReplaced =
|
boolean wasCurrentItemReplaced =
|
||||||
playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex
|
playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex >= fromIndex
|
||||||
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex;
|
&& playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex < toIndex;
|
||||||
@ -2111,6 +2180,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
newTimeline,
|
newTimeline,
|
||||||
newWindowIndex,
|
newWindowIndex,
|
||||||
newPeriodIndex,
|
newPeriodIndex,
|
||||||
|
getCurrentPosition(),
|
||||||
|
getContentPosition(),
|
||||||
Player.DISCONTINUITY_REASON_INTERNAL);
|
Player.DISCONTINUITY_REASON_INTERNAL);
|
||||||
|
|
||||||
updatePlayerInfo(
|
updatePlayerInfo(
|
||||||
@ -2972,6 +3043,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
Timeline timeline,
|
Timeline timeline,
|
||||||
int newMediaItemIndex,
|
int newMediaItemIndex,
|
||||||
int newPeriodIndex,
|
int newPeriodIndex,
|
||||||
|
long newPositionMs,
|
||||||
|
long newContentPositionMs,
|
||||||
int discontinuityReason) {
|
int discontinuityReason) {
|
||||||
PositionInfo newPositionInfo =
|
PositionInfo newPositionInfo =
|
||||||
new PositionInfo(
|
new PositionInfo(
|
||||||
@ -2980,8 +3053,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
timeline.getWindow(newMediaItemIndex, new Window()).mediaItem,
|
timeline.getWindow(newMediaItemIndex, new Window()).mediaItem,
|
||||||
/* periodUid= */ null,
|
/* periodUid= */ null,
|
||||||
newPeriodIndex,
|
newPeriodIndex,
|
||||||
playerInfo.sessionPositionInfo.positionInfo.positionMs,
|
newPositionMs,
|
||||||
playerInfo.sessionPositionInfo.positionInfo.contentPositionMs,
|
newContentPositionMs,
|
||||||
playerInfo.sessionPositionInfo.positionInfo.adGroupIndex,
|
playerInfo.sessionPositionInfo.positionInfo.adGroupIndex,
|
||||||
playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup);
|
playerInfo.sessionPositionInfo.positionInfo.adIndexInAdGroup);
|
||||||
return maskTimelineAndPositionInfo(
|
return maskTimelineAndPositionInfo(
|
||||||
|
@ -244,7 +244,8 @@ import java.util.List;
|
|||||||
long lastSetPlayWhenReadyCalledTimeMs,
|
long lastSetPlayWhenReadyCalledTimeMs,
|
||||||
long timeDiffMs) {
|
long timeDiffMs) {
|
||||||
boolean receivedUpdatedPositionInfo =
|
boolean receivedUpdatedPositionInfo =
|
||||||
lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs;
|
playerInfo.sessionPositionInfo.equals(SessionPositionInfo.DEFAULT)
|
||||||
|
|| (lastSetPlayWhenReadyCalledTimeMs < playerInfo.sessionPositionInfo.eventTimeMs);
|
||||||
if (!playerInfo.isPlaying) {
|
if (!playerInfo.isPlaying) {
|
||||||
if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) {
|
if (receivedUpdatedPositionInfo || currentPositionMs == C.TIME_UNSET) {
|
||||||
return playerInfo.sessionPositionInfo.positionInfo.positionMs;
|
return playerInfo.sessionPositionInfo.positionInfo.positionMs;
|
||||||
|
@ -2348,6 +2348,7 @@ public class MediaControllerStateMaskingTest {
|
|||||||
createMediaItems(firstMediaId, secondMediaId, thirdMediaId)))
|
createMediaItems(firstMediaId, secondMediaId, thirdMediaId)))
|
||||||
.setCurrentMediaItemIndex(initialMediaItemIndex)
|
.setCurrentMediaItemIndex(initialMediaItemIndex)
|
||||||
.setCurrentPeriodIndex(initialMediaItemIndex)
|
.setCurrentPeriodIndex(initialMediaItemIndex)
|
||||||
|
.setCurrentPosition(2000L)
|
||||||
.build();
|
.build();
|
||||||
remoteSession.setPlayer(playerConfig);
|
remoteSession.setPlayer(playerConfig);
|
||||||
|
|
||||||
@ -2409,6 +2410,7 @@ public class MediaControllerStateMaskingTest {
|
|||||||
/* testLastPeriodIndex= */ testCurrentMediaItemIndex);
|
/* testLastPeriodIndex= */ testCurrentMediaItemIndex);
|
||||||
assertThat(newMediaItemRef.get().mediaId).isEqualTo(testCurrentMediaId);
|
assertThat(newMediaItemRef.get().mediaId).isEqualTo(testCurrentMediaId);
|
||||||
assertThat(newPositionInfoRef.get().mediaItemIndex).isEqualTo(testCurrentMediaItemIndex);
|
assertThat(newPositionInfoRef.get().mediaItemIndex).isEqualTo(testCurrentMediaItemIndex);
|
||||||
|
assertThat(newPositionInfoRef.get().positionMs).isEqualTo(0L);
|
||||||
assertThat(getEventsAsList(onEventsRef.get()))
|
assertThat(getEventsAsList(onEventsRef.get()))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
Player.EVENT_TIMELINE_CHANGED,
|
Player.EVENT_TIMELINE_CHANGED,
|
||||||
@ -3439,12 +3441,14 @@ public class MediaControllerStateMaskingTest {
|
|||||||
new RemoteMediaSession.MockPlayerConfigBuilder()
|
new RemoteMediaSession.MockPlayerConfigBuilder()
|
||||||
.setTimeline(MediaTestUtils.createTimeline(3))
|
.setTimeline(MediaTestUtils.createTimeline(3))
|
||||||
.setCurrentMediaItemIndex(1)
|
.setCurrentMediaItemIndex(1)
|
||||||
|
.setCurrentPosition(2000L)
|
||||||
.build();
|
.build();
|
||||||
remoteSession.setPlayer(playerConfig);
|
remoteSession.setPlayer(playerConfig);
|
||||||
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
|
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
|
||||||
CountDownLatch latch = new CountDownLatch(2);
|
CountDownLatch latch = new CountDownLatch(2);
|
||||||
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
|
AtomicReference<Timeline> newTimelineRef = new AtomicReference<>();
|
||||||
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
|
AtomicReference<Player.Events> onEventsRef = new AtomicReference<>();
|
||||||
|
AtomicReference<PositionInfo> newPositionInfoRef = new AtomicReference<>();
|
||||||
Player.Listener listener =
|
Player.Listener listener =
|
||||||
new Player.Listener() {
|
new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
@ -3458,6 +3462,14 @@ public class MediaControllerStateMaskingTest {
|
|||||||
onEventsRef.set(events);
|
onEventsRef.set(events);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPositionDiscontinuity(
|
||||||
|
PositionInfo oldPosition,
|
||||||
|
PositionInfo newPosition,
|
||||||
|
@Player.DiscontinuityReason int reason) {
|
||||||
|
newPositionInfoRef.set(newPosition);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
|
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
|
||||||
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
|
AtomicInteger currentMediaItemIndexRef = new AtomicInteger();
|
||||||
@ -3478,6 +3490,7 @@ public class MediaControllerStateMaskingTest {
|
|||||||
Player.EVENT_TIMELINE_CHANGED,
|
Player.EVENT_TIMELINE_CHANGED,
|
||||||
Player.EVENT_POSITION_DISCONTINUITY,
|
Player.EVENT_POSITION_DISCONTINUITY,
|
||||||
Player.EVENT_MEDIA_ITEM_TRANSITION);
|
Player.EVENT_MEDIA_ITEM_TRANSITION);
|
||||||
|
assertThat(newPositionInfoRef.get().positionMs).isEqualTo(2000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user