Update session position info on timeline change

This fixes an inconsistent state of the `PlayerInfo` when the index of the playing
media item is changed by a playlist modification. In this inconsistent state,
calling `Playerinfo.getCurrentMediaItem` can produce an
`ArrayIndexOutOfBoundException` (see stack trace in GH issue).

This change takes the following measurements:

- always update sessionPosition and timeline of the PlayerInfo together in
  `MediaSessionImpl.PlayerListener` where the PlayerInfo originates from
- add an assertion to avoid building a `PlayerInfo` instance in an inconsistent
  state
- reduce the window of opportunity for concurrent access to
  `mediaSessionImpl.playerInfo` when dispatching player info changes in
  `MediaSessionImpl`

Issue: androidx/media#51
PiperOrigin-RevId: 444812661
This commit is contained in:
bachinger 2022-04-27 11:48:01 +01:00 committed by Ian Baker
parent 9369348d6f
commit dee83cc7db
4 changed files with 39 additions and 17 deletions

View File

@ -80,6 +80,9 @@
* Session:
* Fix NPE in MediaControllerImplLegacy
([#59](https://github.com/androidx/media/pull/59))
* Update session position info on timeline
change([#51](https://github.com/androidx/media/issues/51))
* Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Remove deprecated symbols:

View File

@ -753,7 +753,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int newCurrentMediaItemIndex =
calculateCurrentItemIndexAfterAddItems(currentMediaItemIndex, index, mediaItems.size());
PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex);
controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo =
new ControllerInfo(
maskedPlayerInfo,
@ -801,7 +802,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
+ " new current item");
}
PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex);
controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo =
new ControllerInfo(
@ -861,7 +863,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
QueueTimeline newQueueTimeline =
queueTimeline.copyWithMovedMediaItems(fromIndex, toIndex, newIndex);
PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimeline(newQueueTimeline, newCurrentMediaItemIndex);
controllerInfo.playerInfo.copyWithTimelineAndMediaItemIndex(
newQueueTimeline, newCurrentMediaItemIndex);
ControllerInfo maskedControllerInfo =
new ControllerInfo(

View File

@ -366,7 +366,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
(controller, seq) -> controller.sendCustomCommand(seq, command, args));
}
private void dispatchOnPlayerInfoChanged(boolean excludeTimeline) {
private void dispatchOnPlayerInfoChanged(PlayerInfo playerInfo, boolean excludeTimeline) {
List<ControllerInfo> controllers =
sessionStub.getConnectedControllersManager().getConnectedControllers();
@ -910,7 +910,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (player == null) {
return;
}
session.playerInfo = session.playerInfo.copyWithTimeline(timeline);
session.playerInfo =
session.playerInfo.copyWithTimelineAndSessionPositionInfo(
timeline, player.createSessionPositionInfoForBundling());
session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(/* excludeTimeline= */ false);
session.dispatchRemoteControllerTaskToLegacyStub(
(callback, seq) -> callback.onTimelineChanged(seq, timeline, reason));
@ -1177,9 +1179,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public void handleMessage(Message msg) {
if (msg.what == MSG_PLAYER_INFO_CHANGED) {
playerInfo =
playerInfo.copyWithSessionPositionInfo(
playerInfo.copyWithTimelineAndSessionPositionInfo(
getPlayerWrapper().getCurrentTimeline(),
getPlayerWrapper().createSessionPositionInfoForBundling());
dispatchOnPlayerInfoChanged(excludeTimeline);
dispatchOnPlayerInfoChanged(playerInfo, excludeTimeline);
excludeTimeline = true;
} else {
throw new IllegalStateException("Invalid message what=" + msg.what);

View File

@ -44,6 +44,7 @@ import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleableUtil;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
@ -271,6 +272,9 @@ import java.util.List;
}
public PlayerInfo build() {
Assertions.checkState(
timeline.isEmpty()
|| sessionPositionInfo.positionInfo.mediaItemIndex < timeline.getWindowCount());
return new PlayerInfo(
playerError,
mediaItemTransitionReason,
@ -344,7 +348,7 @@ import java.util.List;
MediaMetadata.EMPTY,
/* seekBackIncrementMs= */ 0,
/* seekForwardIncrementMs= */ 0,
/* maxSeekToPreviousPosition= */ 0,
/* maxSeekToPreviousPositionMs= */ 0,
TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT);
@Nullable public final PlaybackException playerError;
@ -429,11 +433,6 @@ import java.util.List;
return new Builder(this).setPlayerError(playerError).build();
}
@CheckResult
public PlayerInfo copyWithSessionPositionInfo(SessionPositionInfo sessionPositionInfo) {
return new Builder(this).setSessionPositionInfo(sessionPositionInfo).build();
}
@CheckResult
public PlayerInfo copyWithPlaybackState(
@Player.State int playbackState, @Nullable PlaybackException playerError) {
@ -454,6 +453,11 @@ import java.util.List;
return new Builder(this).setIsLoading(isLoading).build();
}
@CheckResult
public PlayerInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) {
return new Builder(this).setPlaybackParameters(playbackParameters).build();
}
@CheckResult
public PlayerInfo copyWithPositionInfos(
PositionInfo oldPositionInfo,
@ -467,8 +471,8 @@ import java.util.List;
}
@CheckResult
public PlayerInfo copyWithPlaybackParameters(PlaybackParameters playbackParameters) {
return new Builder(this).setPlaybackParameters(playbackParameters).build();
public PlayerInfo copyWithSessionPositionInfo(SessionPositionInfo sessionPositionInfo) {
return new Builder(this).setSessionPositionInfo(sessionPositionInfo).build();
}
@CheckResult
@ -477,14 +481,23 @@ import java.util.List;
}
@CheckResult
public PlayerInfo copyWithTimeline(Timeline timeline, int windowIndex) {
public PlayerInfo copyWithTimelineAndSessionPositionInfo(
Timeline timeline, SessionPositionInfo sessionPositionInfo) {
return new Builder(this)
.setTimeline(timeline)
.setSessionPositionInfo(sessionPositionInfo)
.build();
}
@CheckResult
public PlayerInfo copyWithTimelineAndMediaItemIndex(Timeline timeline, int mediaItemIndex) {
return new Builder(this)
.setTimeline(timeline)
.setSessionPositionInfo(
new SessionPositionInfo(
new PositionInfo(
sessionPositionInfo.positionInfo.windowUid,
windowIndex,
mediaItemIndex,
sessionPositionInfo.positionInfo.mediaItem,
sessionPositionInfo.positionInfo.periodUid,
sessionPositionInfo.positionInfo.periodIndex,