Skip to content

Commit dddc602

Browse files
toniheimicrokatz
authored andcommitted
Wait with PlayerInfo updates until all pending operations are done
Accepting a PlayerInfo while the MediaController is masking its state means we are reverting all masking changes we've made earlier. This only makes sense if the update already contains the masked operation. If multiple operations are in flight (or are sent from the session while they are in flight), we need to wait until all of them are handled before accepting new updates. In cases where a new update from the session excludes the Timeline and the masked state is incompatible with the new update, we also risk an exception if we accept the update too early. PiperOrigin-RevId: 487266899 (cherry picked from commit 0b4ba3e)
1 parent bc3aef0 commit dddc602

File tree

3 files changed

+121
-6
lines changed

3 files changed

+121
-6
lines changed

libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import android.view.SurfaceView;
4848
import android.view.TextureView;
4949
import androidx.annotation.Nullable;
50+
import androidx.collection.ArraySet;
5051
import androidx.media3.common.AdPlaybackState;
5152
import androidx.media3.common.AudioAttributes;
5253
import androidx.media3.common.BundleListRetriever;
@@ -110,6 +111,7 @@
110111
private final SurfaceCallback surfaceCallback;
111112
private final ListenerSet<Listener> listeners;
112113
private final FlushCommandQueueHandler flushCommandQueueHandler;
114+
private final ArraySet<Integer> pendingMaskingSequencedFutureNumbers;
113115

114116
@Nullable private SessionToken connectedToken;
115117
@Nullable private SessionServiceConnection serviceConnection;
@@ -127,6 +129,7 @@
127129
@Nullable private IMediaSession iSession;
128130
private long lastReturnedCurrentPositionMs;
129131
private long lastSetPlayWhenReadyCalledTimeMs;
132+
@Nullable private Timeline pendingPlayerInfoUpdateTimeline;
130133

131134
public MediaControllerImplBase(
132135
Context context,
@@ -154,6 +157,7 @@ public MediaControllerImplBase(
154157
this.context = context;
155158
sequencedFutureManager = new SequencedFutureManager();
156159
controllerStub = new MediaControllerStub(this);
160+
pendingMaskingSequencedFutureNumbers = new ArraySet<>();
157161
this.token = token;
158162
this.connectionHints = connectionHints;
159163
deathRecipient =
@@ -303,7 +307,7 @@ private ListenableFuture dispatchRemoteSessionTaskWithPlayerComma
303307
if (command != Player.COMMAND_SET_VIDEO_SURFACE) {
304308
flushCommandQueueHandler.sendFlushCommandQueueMessage();
305309
}
306-
return dispatchRemoteSessionTask(iSession, task);
310+
return dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true);
307311
}
308312

309313
private ListenableFuture<SessionResult> dispatchRemoteSessionTaskWithSessionCommand(
@@ -326,17 +330,22 @@ private ListenableFuture dispatchRemoteSessionTaskWithSessionComm
326330
sessionCommand != null
327331
? getSessionInterfaceWithSessionCommandIfAble(sessionCommand)
328332
: getSessionInterfaceWithSessionCommandIfAble(commandCode),
329-
task);
333+
task,
334+
/* addToPendingMaskingOperations= */ false);
330335
}
331336

332337
private ListenableFuture<SessionResult> dispatchRemoteSessionTask(
333-
IMediaSession iSession, RemoteSessionTask task) {
338+
IMediaSession iSession, RemoteSessionTask task, boolean addToPendingMaskingOperations) {
334339
if (iSession != null) {
335340
SequencedFutureManager.SequencedFuture<SessionResult> result =
336341
sequencedFutureManager.createSequencedFuture(
337342
new SessionResult(SessionResult.RESULT_INFO_SKIPPED));
338343
try {
339-
task.run(iSession, result.getSequenceNumber());
344+
int sequenceNumber = result.getSequenceNumber();
345+
task.run(iSession, sequenceNumber);
346+
if (addToPendingMaskingOperations) {
347+
pendingMaskingSequencedFutureNumbers.add(sequenceNumber);
348+
}
340349
} catch (RemoteException e) {
341350
Log.w(TAG, "Cannot connect to the service or the session is gone", e);
342351
result.set(new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED));
@@ -2223,7 +2232,12 @@ void notifyPeriodicSessionPositionInfoChanged(SessionPositionInfo sessionPositio
22232232
}
22242233

22252234
<T extends @NonNull Object> void setFutureResult(int seq, T futureResult) {
2235+
// Don't set the future result on the application looper so that the result can be obtained by a
2236+
// blocking future.get() on the application looper. But post a message to remove the pending
2237+
// masking operation on the application looper to ensure it's executed in order with other
2238+
// updates sent to the application looper.
22262239
sequencedFutureManager.setFutureResult(seq, futureResult);
2240+
getInstance().runOnApplicationLooper(() -> pendingMaskingSequencedFutureNumbers.remove(seq));
22272241
}
22282242

22292243
void onConnected(ConnectionState result) {
@@ -2313,11 +2327,23 @@ void onPlayerInfoChanged(
23132327
if (!isConnected()) {
23142328
return;
23152329
}
2330+
if (!pendingMaskingSequencedFutureNumbers.isEmpty()) {
2331+
// We are still waiting for all pending masking operations to be handled.
2332+
if (!isTimelineExcluded) {
2333+
pendingPlayerInfoUpdateTimeline = newPlayerInfo.timeline;
2334+
}
2335+
return;
2336+
}
23162337
PlayerInfo oldPlayerInfo = playerInfo;
23172338
playerInfo = newPlayerInfo;
23182339
if (isTimelineExcluded) {
2319-
playerInfo = playerInfo.copyWithTimeline(oldPlayerInfo.timeline);
2340+
playerInfo =
2341+
playerInfo.copyWithTimeline(
2342+
pendingPlayerInfoUpdateTimeline != null
2343+
? pendingPlayerInfoUpdateTimeline
2344+
: oldPlayerInfo.timeline);
23202345
}
2346+
pendingPlayerInfoUpdateTimeline = null;
23212347
PlaybackException oldPlayerError = oldPlayerInfo.playerError;
23222348
PlaybackException playerError = playerInfo.playerError;
23232349
boolean errorsMatch =
@@ -2568,7 +2594,8 @@ public void onRenderedFirstFrame() {
25682594
}
25692595

25702596
private void updateSessionPositionInfoIfNeeded(SessionPositionInfo sessionPositionInfo) {
2571-
if (playerInfo.sessionPositionInfo.eventTimeMs < sessionPositionInfo.eventTimeMs) {
2597+
if (pendingMaskingSequencedFutureNumbers.isEmpty()
2598+
&& playerInfo.sessionPositionInfo.eventTimeMs < sessionPositionInfo.eventTimeMs) {
25722599
playerInfo = playerInfo.copyWithSessionPositionInfo(sessionPositionInfo);
25732600
}
25742601
}

libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerStateMaskingTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,85 @@ public void moveMediaItems_moveNonCurrentItem_fromBeforeCurrentItemToAfter() thr
28782878
/* testPreviousMediaItemIndex= */ C.INDEX_UNSET);
28792879
}
28802880

2881+
@Test
2882+
public void incompatibleUpdatesDuringMasking_areOnlyReportedOnceAllPendingUpdatesAreResolved()
2883+
throws Exception {
2884+
// Test setup:
2885+
// 1. Report a discontinuity from item 0 to item 1 in the session.
2886+
// 2. Before (1) can be handled by the controller, remove item 1.
2887+
// Expectation:
2888+
// - Session: State is updated to ENDED as the current item is removed.
2889+
// - Controller: Discontinuity is only reported after the state is fully resolved
2890+
// = The discontinuity is only reported once we also report the state change to ENDED.
2891+
Timeline timeline = MediaTestUtils.createTimeline(/* windowCount= */ 2);
2892+
remoteSession.getMockPlayer().setTimeline(timeline);
2893+
remoteSession
2894+
.getMockPlayer()
2895+
.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
2896+
remoteSession.getMockPlayer().setCurrentMediaItemIndex(0);
2897+
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
2898+
CountDownLatch positionDiscontinuityReported = new CountDownLatch(1);
2899+
AtomicBoolean reportedStateChangeToEndedAtSameTimeAsDiscontinuity = new AtomicBoolean();
2900+
Player.Listener listener =
2901+
new Player.Listener() {
2902+
@Override
2903+
public void onEvents(Player player, Player.Events events) {
2904+
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
2905+
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
2906+
&& player.getPlaybackState() == Player.STATE_ENDED) {
2907+
reportedStateChangeToEndedAtSameTimeAsDiscontinuity.set(true);
2908+
}
2909+
positionDiscontinuityReported.countDown();
2910+
}
2911+
}
2912+
};
2913+
threadTestRule.getHandler().postAndSync(() -> controller.addListener(listener));
2914+
2915+
// Step 1: Report a discontinuity from item 0 to item 1 in the session.
2916+
PositionInfo oldPositionInfo =
2917+
new PositionInfo(
2918+
/* windowUid= */ timeline.getWindow(/* windowIndex= */ 0, new Window()).uid,
2919+
/* mediaItemIndex= */ 0,
2920+
MediaItem.EMPTY,
2921+
/* periodUid= */ timeline.getPeriod(
2922+
/* periodIndex= */ 0, new Period(), /* setIds= */ true)
2923+
.uid,
2924+
/* periodIndex= */ 0,
2925+
/* positionMs= */ 10_000,
2926+
/* contentPositionMs= */ 10_000,
2927+
/* adGroupIndex= */ C.INDEX_UNSET,
2928+
/* adIndexInAdGroup= */ C.INDEX_UNSET);
2929+
PositionInfo newPositionInfo =
2930+
new PositionInfo(
2931+
/* windowUid= */ timeline.getWindow(/* windowIndex= */ 1, new Window()).uid,
2932+
/* mediaItemIndex= */ 1,
2933+
MediaItem.EMPTY,
2934+
/* periodUid= */ timeline.getPeriod(
2935+
/* periodIndex= */ 1, new Period(), /* setIds= */ true)
2936+
.uid,
2937+
/* periodIndex= */ 1,
2938+
/* positionMs= */ 0,
2939+
/* contentPositionMs= */ 0,
2940+
/* adGroupIndex= */ C.INDEX_UNSET,
2941+
/* adIndexInAdGroup= */ C.INDEX_UNSET);
2942+
remoteSession.getMockPlayer().setCurrentMediaItemIndex(1);
2943+
remoteSession
2944+
.getMockPlayer()
2945+
.notifyPositionDiscontinuity(
2946+
oldPositionInfo, newPositionInfo, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
2947+
// Step 2: Before step 1 can be handled by the controller, remove item 1.
2948+
threadTestRule.getHandler().postAndSync(() -> controller.removeMediaItem(/* index= */ 1));
2949+
remoteSession.getMockPlayer().setCurrentMediaItemIndex(0);
2950+
remoteSession.getMockPlayer().setTimeline(MediaTestUtils.createTimeline(/* windowCount= */ 1));
2951+
remoteSession.getMockPlayer().notifyPlaybackStateChanged(Player.STATE_ENDED);
2952+
remoteSession
2953+
.getMockPlayer()
2954+
.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
2955+
2956+
assertThat(positionDiscontinuityReported.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
2957+
assertThat(reportedStateChangeToEndedAtSameTimeAsDiscontinuity.get()).isTrue();
2958+
}
2959+
28812960
private void assertMoveMediaItems(
28822961
int initialMediaItemCount,
28832962
int initialMediaItemIndex,

libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,13 @@ public void setTimeline(String sessionId, Bundle timelineBundle) throws RemoteEx
785785
MediaSession session = sessionMap.get(sessionId);
786786
MockPlayer player = (MockPlayer) session.getPlayer();
787787
player.timeline = Timeline.CREATOR.fromBundle(timelineBundle);
788+
List<MediaItem> mediaItems = new ArrayList<>();
789+
for (int i = 0; i < player.timeline.getWindowCount(); i++) {
790+
mediaItems.add(
791+
player.timeline.getWindow(/* windowIndex= */ i, new Timeline.Window()).mediaItem);
792+
}
793+
player.mediaItems.clear();
794+
player.mediaItems.addAll(mediaItems);
788795
});
789796
}
790797

@@ -800,6 +807,8 @@ public void createAndSetFakeTimeline(String sessionId, int windowCount) throws R
800807
mediaItems.add(
801808
MediaTestUtils.createMediaItem(TestUtils.getMediaIdInFakeTimeline(windowIndex)));
802809
}
810+
player.mediaItems.clear();
811+
player.mediaItems.addAll(mediaItems);
803812
player.timeline = new PlaylistTimeline(mediaItems);
804813
});
805814
}

0 commit comments

Comments
 (0)