Commit 7cb7636e by tonihei Committed by Rohit Singh

Run MediaSessionStub commands in order

Some commands are run asynchronously and subsequent commands need
to wait until the previous one finished. This can be supported
by returning a Future for each command and using the existing
command execution logic to wait for each Future to complete.

As some MediaSessionStub code is now executed delayed to when it
was originally created, we also need to check if the session is
not released before triggering any actions or sending result codes.

Issue: androidx/media#85
PiperOrigin-RevId: 462101136
parent 45f1f5b3
...@@ -15,6 +15,10 @@ ...@@ -15,6 +15,10 @@
`MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory,
boolean)` to specify whether the renderer will output metadata early or boolean)` to specify whether the renderer will output metadata early or
in sync with the player position. in sync with the player position.
* Session:
* Ensure commands are always executed in the correct order even if some
require asynchronous resolution
([#85](https://github.com/androidx/media/issues/85)).
### 1.0.0-beta02 (2022-07-15) ### 1.0.0-beta02 (2022-07-15)
......
...@@ -24,7 +24,6 @@ import androidx.collection.ArrayMap; ...@@ -24,7 +24,6 @@ import androidx.collection.ArrayMap;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayDeque; import java.util.ArrayDeque;
...@@ -227,15 +226,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; ...@@ -227,15 +226,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
} }
public void addToCommandQueue(ControllerInfo controllerInfo, Runnable commandRunnable) { public void addToCommandQueue(ControllerInfo controllerInfo, AsyncCommand asyncCommand) {
synchronized (lock) { synchronized (lock) {
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo); @Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
if (info != null) { if (info != null) {
info.commandQueue.add( info.commandQueue.add(asyncCommand);
() -> {
commandRunnable.run();
return Futures.immediateVoidFuture();
});
} }
} }
} }
......
...@@ -81,6 +81,7 @@ interface IRemoteMediaController { ...@@ -81,6 +81,7 @@ interface IRemoteMediaController {
void release(String controllerId); void release(String controllerId);
void stop(String controllerId); void stop(String controllerId);
void setTrackSelectionParameters(String controllerId, in Bundle parameters); void setTrackSelectionParameters(String controllerId, in Bundle parameters);
void setMediaItemsPreparePlayAddItemsSeek(String controllerId, in List<Bundle> initialMediaItems, in List<Bundle> addedMediaItems, int seekIndex);
// MediaBrowser methods // MediaBrowser methods
Bundle getLibraryRoot(String controllerId, in Bundle libraryParams); Bundle getLibraryRoot(String controllerId, in Bundle libraryParams);
......
...@@ -19,19 +19,21 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA ...@@ -19,19 +19,21 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.media3.common.DeviceInfo; import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.util.Util;
import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.HandlerThreadTestRule;
import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.MainLooperTestRule;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.List; import java.util.List;
import org.junit.After; import org.junit.After;
...@@ -58,6 +60,7 @@ public class MediaSessionPlayerTest { ...@@ -58,6 +60,7 @@ public class MediaSessionPlayerTest {
private MediaSession session; private MediaSession session;
private MockPlayer player; private MockPlayer player;
private RemoteMediaController controller; private RemoteMediaController controller;
private HandlerThread asyncHandlerThread;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
...@@ -66,6 +69,9 @@ public class MediaSessionPlayerTest { ...@@ -66,6 +69,9 @@ public class MediaSessionPlayerTest {
.setApplicationLooper(threadTestRule.getHandler().getLooper()) .setApplicationLooper(threadTestRule.getHandler().getLooper())
.setMediaItems(/* itemCount= */ 5) .setMediaItems(/* itemCount= */ 5)
.build(); .build();
asyncHandlerThread = new HandlerThread("AsyncHandlerThread");
asyncHandlerThread.start();
Handler asyncHandler = new Handler(asyncHandlerThread.getLooper());
session = session =
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
.setCallback( .setCallback(
...@@ -84,7 +90,9 @@ public class MediaSessionPlayerTest { ...@@ -84,7 +90,9 @@ public class MediaSessionPlayerTest {
MediaSession mediaSession, MediaSession mediaSession,
MediaSession.ControllerInfo controller, MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) { List<MediaItem> mediaItems) {
return Futures.immediateFuture(mediaItems); // Send empty message and return mediaItems once done to simulate asynchronous
// media item resolution.
return Util.postOrRunWithCompletion(asyncHandler, () -> {}, mediaItems);
} }
}) })
.build(); .build();
...@@ -97,6 +105,7 @@ public class MediaSessionPlayerTest { ...@@ -97,6 +105,7 @@ public class MediaSessionPlayerTest {
public void tearDown() throws Exception { public void tearDown() throws Exception {
controller.release(); controller.release();
session.release(); session.release();
asyncHandlerThread.quit();
} }
@Test @Test
...@@ -544,6 +553,26 @@ public class MediaSessionPlayerTest { ...@@ -544,6 +553,26 @@ public class MediaSessionPlayerTest {
assertThat(player.trackSelectionParameters).isEqualTo(trackSelectionParameters); assertThat(player.trackSelectionParameters).isEqualTo(trackSelectionParameters);
} }
@Test
public void mixedAsyncAndSyncCommands_calledInCorrectOrder() throws Exception {
List<MediaItem> initialItems = MediaTestUtils.createMediaItems(/* size= */ 2);
List<MediaItem> addedItems = MediaTestUtils.createMediaItems(/* size= */ 3);
controller.setMediaItemsPreparePlayAddItemsSeek(initialItems, addedItems, /* seekIndex= */ 3);
player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS);
boolean setMediaItemsCalledBeforePrepare =
player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS);
player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS);
boolean addMediaItemsCalledBeforeSeek =
player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS);
assertThat(setMediaItemsCalledBeforePrepare).isTrue();
assertThat(addMediaItemsCalledBeforeSeek).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(player.mediaItems).hasSize(5);
assertThat(player.seekMediaItemIndex).isEqualTo(3);
}
private void changePlaybackTypeToRemote() throws Exception { private void changePlaybackTypeToRemote() throws Exception {
threadTestRule threadTestRule
.getHandler() .getHandler()
......
...@@ -659,6 +659,26 @@ public class MediaControllerProviderService extends Service { ...@@ -659,6 +659,26 @@ public class MediaControllerProviderService extends Service {
}); });
} }
@Override
public void setMediaItemsPreparePlayAddItemsSeek(
String controllerId,
List<Bundle> initialMediaItems,
List<Bundle> addedMediaItems,
int seekIndex)
throws RemoteException {
runOnHandler(
() -> {
MediaController controller = mediaControllerMap.get(controllerId);
controller.setMediaItems(
BundleableUtil.fromBundleList(MediaItem.CREATOR, initialMediaItems));
controller.prepare();
controller.play();
controller.addMediaItems(
BundleableUtil.fromBundleList(MediaItem.CREATOR, addedMediaItems));
controller.seekTo(seekIndex, /* positionMs= */ 0);
});
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// MediaBrowser methods // MediaBrowser methods
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
......
...@@ -292,6 +292,16 @@ public class RemoteMediaController { ...@@ -292,6 +292,16 @@ public class RemoteMediaController {
binder.setTrackSelectionParameters(controllerId, parameters.toBundle()); binder.setTrackSelectionParameters(controllerId, parameters.toBundle());
} }
public void setMediaItemsPreparePlayAddItemsSeek(
List<MediaItem> initialMediaItems, List<MediaItem> addedMediaItems, int seekIndex)
throws RemoteException {
binder.setMediaItemsPreparePlayAddItemsSeek(
controllerId,
BundleableUtil.toBundleList(initialMediaItems),
BundleableUtil.toBundleList(addedMediaItems),
seekIndex);
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Non-public methods // Non-public methods
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment